Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- Fixed Review Agent failing on private GitHub repositories when fetching the PR diff. The parser now uses the REST API endpoint with the `diff` media type instead of the web `diff_url` which is not accessible to API tokens. [#1302](https://github.com/sourcebot-dev/sourcebot/pull/1302)

Comment thread
AasheeshLikePanner marked this conversation as resolved.
## [5.0.2] - 2026-06-11

### Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { expect, test, vi, describe } from 'vitest';
import { Octokit } from 'octokit';
import { githubPrParser } from './githubPrParser';
import { GitHubPullRequest } from '../types';
import { expect, test, vi, describe } from "vitest";
import { Octokit } from "octokit";
import { githubPrParser } from "./githubPrParser";
import { GitHubPullRequest } from "../types";

vi.mock('@sourcebot/shared', () => ({
vi.mock("@sourcebot/shared", () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
Expand All @@ -13,23 +13,25 @@ vi.mock('@sourcebot/shared', () => ({
}));

// Minimal shape satisfying the fields accessed by githubPrParser
function makePullRequest(overrides: Partial<{
number: number;
title: string;
body: string | null;
head_sha: string;
owner: string;
repo: string;
diff_url: string;
}> = {}): GitHubPullRequest {
function makePullRequest(
overrides: Partial<{
number: number;
title: string;
body: string | null;
head_sha: string;
owner: string;
repo: string;
diff_url: string;
}> = {},
): GitHubPullRequest {
const opts = {
number: 7,
title: 'My PR title',
body: 'My PR description',
head_sha: 'sha_abc123',
owner: 'my-org',
repo: 'my-repo',
diff_url: 'https://github.com/my-org/my-repo/pull/7.diff',
title: "My PR title",
body: "My PR description",
head_sha: "sha_abc123",
owner: "my-org",
repo: "my-repo",
diff_url: "https://github.com/my-org/my-repo/pull/7.diff",
...overrides,
};

Expand All @@ -54,140 +56,154 @@ function makeMockOctokit(diffText: string) {
} as unknown as Octokit;
}

describe('githubPrParser', () => {
test('maps pull request metadata correctly', async () => {
const octokit = makeMockOctokit('');
describe("githubPrParser", () => {
test("maps pull request metadata correctly", async () => {
const octokit = makeMockOctokit("");
const pr = makePullRequest();

const result = await githubPrParser(octokit, pr);

expect(result.title).toBe('My PR title');
expect(result.description).toBe('My PR description');
expect(result.title).toBe("My PR title");
expect(result.description).toBe("My PR description");
expect(result.number).toBe(7);
expect(result.head_sha).toBe('sha_abc123');
expect(result.owner).toBe('my-org');
expect(result.repo).toBe('my-repo');
expect(result.hostDomain).toBe('github.com');
expect(result.head_sha).toBe("sha_abc123");
expect(result.owner).toBe("my-org");
expect(result.repo).toBe("my-repo");
expect(result.hostDomain).toBe("github.com");
});

test('uses empty string when body is null', async () => {
const octokit = makeMockOctokit('');
test("uses empty string when body is null", async () => {
const octokit = makeMockOctokit("");
const pr = makePullRequest({ body: null });

const result = await githubPrParser(octokit, pr);

expect(result.description).toBe('');
expect(result.description).toBe("");
});

test('fetches diff using the pull request diff_url', async () => {
const mockRequest = vi.fn().mockResolvedValue({ data: '' });
test("fetches diff using the REST API endpoint instead of the web diff_url", async () => {
const mockRequest = vi.fn().mockResolvedValue({ data: "" });
const octokit = { request: mockRequest } as unknown as Octokit;
const pr = makePullRequest({ diff_url: 'https://github.com/my-org/my-repo/pull/7.diff' });
const pr = makePullRequest({
owner: "my-org",
repo: "my-repo",
number: 7,
});

await githubPrParser(octokit, pr);

expect(mockRequest).toHaveBeenCalledWith('https://github.com/my-org/my-repo/pull/7.diff');
expect(mockRequest).toHaveBeenCalledWith(
"GET /repos/{owner}/{repo}/pulls/{pull_number}",
{
owner: "my-org",
repo: "my-repo",
pull_number: 7,
mediaType: { format: "diff" },
},
);
});

test('returns empty file_diffs for an empty diff', async () => {
const octokit = makeMockOctokit('');
test("returns empty file_diffs for an empty diff", async () => {
const octokit = makeMockOctokit("");
const pr = makePullRequest();

const result = await githubPrParser(octokit, pr);

expect(result.file_diffs).toEqual([]);
});

test('parses a unified diff with added and context lines', async () => {
test("parses a unified diff with added and context lines", async () => {
const unifiedDiff = [
'diff --git a/src/foo.ts b/src/foo.ts',
'--- a/src/foo.ts',
'+++ b/src/foo.ts',
'@@ -1,2 +1,3 @@',
' context line',
'+added line',
' another context',
].join('\n');
"diff --git a/src/foo.ts b/src/foo.ts",
"--- a/src/foo.ts",
"+++ b/src/foo.ts",
"@@ -1,2 +1,3 @@",
" context line",
"+added line",
" another context",
].join("\n");
const octokit = makeMockOctokit(unifiedDiff);
const pr = makePullRequest();

const result = await githubPrParser(octokit, pr);

expect(result.file_diffs).toHaveLength(1);
expect(result.file_diffs[0].from).toBe('src/foo.ts');
expect(result.file_diffs[0].to).toBe('src/foo.ts');
expect(result.file_diffs[0].from).toBe("src/foo.ts");
expect(result.file_diffs[0].to).toBe("src/foo.ts");
expect(result.file_diffs[0].diffs).toHaveLength(1);
});

test('newSnippet contains added lines and context', async () => {
test("newSnippet contains added lines and context", async () => {
const unifiedDiff = [
'diff --git a/src/foo.ts b/src/foo.ts',
'--- a/src/foo.ts',
'+++ b/src/foo.ts',
'@@ -1,1 +1,2 @@',
' context',
'+new line here',
].join('\n');
"diff --git a/src/foo.ts b/src/foo.ts",
"--- a/src/foo.ts",
"+++ b/src/foo.ts",
"@@ -1,1 +1,2 @@",
" context",
"+new line here",
].join("\n");
const octokit = makeMockOctokit(unifiedDiff);
const pr = makePullRequest();

const result = await githubPrParser(octokit, pr);
const diff = result.file_diffs[0].diffs[0];

expect(diff.newSnippet).toContain('+new line here');
expect(diff.oldSnippet).not.toContain('+new line here');
expect(diff.newSnippet).toContain("+new line here");
expect(diff.oldSnippet).not.toContain("+new line here");
});

test('oldSnippet contains deleted lines', async () => {
test("oldSnippet contains deleted lines", async () => {
const unifiedDiff = [
'diff --git a/src/foo.ts b/src/foo.ts',
'--- a/src/foo.ts',
'+++ b/src/foo.ts',
'@@ -1,2 +1,1 @@',
' context',
'-removed line',
].join('\n');
"diff --git a/src/foo.ts b/src/foo.ts",
"--- a/src/foo.ts",
"+++ b/src/foo.ts",
"@@ -1,2 +1,1 @@",
" context",
"-removed line",
].join("\n");
const octokit = makeMockOctokit(unifiedDiff);
const pr = makePullRequest();

const result = await githubPrParser(octokit, pr);
const diff = result.file_diffs[0].diffs[0];

expect(diff.oldSnippet).toContain('-removed line');
expect(diff.newSnippet).not.toContain('-removed line');
expect(diff.oldSnippet).toContain("-removed line");
expect(diff.newSnippet).not.toContain("-removed line");
});

test('parses multiple files from a diff', async () => {
test("parses multiple files from a diff", async () => {
const unifiedDiff = [
'diff --git a/src/a.ts b/src/a.ts',
'--- a/src/a.ts',
'+++ b/src/a.ts',
'@@ -1,1 +1,2 @@',
' ctx',
'+add',
'diff --git a/src/b.ts b/src/b.ts',
'--- a/src/b.ts',
'+++ b/src/b.ts',
'@@ -1,2 +1,1 @@',
' ctx',
'-remove',
].join('\n');
"diff --git a/src/a.ts b/src/a.ts",
"--- a/src/a.ts",
"+++ b/src/a.ts",
"@@ -1,1 +1,2 @@",
" ctx",
"+add",
"diff --git a/src/b.ts b/src/b.ts",
"--- a/src/b.ts",
"+++ b/src/b.ts",
"@@ -1,2 +1,1 @@",
" ctx",
"-remove",
].join("\n");
const octokit = makeMockOctokit(unifiedDiff);
const pr = makePullRequest();

const result = await githubPrParser(octokit, pr);

expect(result.file_diffs).toHaveLength(2);
expect(result.file_diffs[0].to).toBe('src/a.ts');
expect(result.file_diffs[1].to).toBe('src/b.ts');
expect(result.file_diffs[0].to).toBe("src/a.ts");
expect(result.file_diffs[1].to).toBe("src/b.ts");
});

test('throws when the diff request fails', async () => {
test("throws when the diff request fails", async () => {
const octokit = {
request: vi.fn().mockRejectedValue(new Error('Network error')),
request: vi.fn().mockRejectedValue(new Error("Network error")),
} as unknown as Octokit;
const pr = makePullRequest();

await expect(githubPrParser(octokit, pr)).rejects.toThrow('Network error');
await expect(githubPrParser(octokit, pr)).rejects.toThrow(
"Network error",
);
});
});
Loading