feat(context): include diffHunk in PR review comment context (#1584)

* feat(context): include diffHunk in PR review comment context

Review comments arrived with only path and line, so the code they were
written against was missing from the prompt. Fetch diffHunk in the PR
GraphQL query and render it under the comment as a diff block.

The hunk is PR-authored content, so it goes through sanitizeContent like
the comment body. Comments without a hunk are unchanged.

Fixes #855

* test(formatter): cover outdated review comments with an empty diff hunk

GitHub returns diffHunk: "" (not null) for comments whose line no longer
exists in the diff, so the render guard has to reject empty strings too.
Found running the real query against anthropics/claude-code-action#1025.
This commit is contained in:
Rishav Naskar
2026-08-14 16:32:44 -07:00
committed by GitHub
parent 9678fce999
commit b49813d0e7
4 changed files with 118 additions and 1 deletions
+1
View File
@@ -84,6 +84,7 @@ export const PR_QUERY = `
body body
path path
line line
diffHunk
author { author {
login login
} }
+10 -1
View File
@@ -118,7 +118,16 @@ export function formatReviewComments(
body = sanitizeContent(body); body = sanitizeContent(body);
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; let formatted = ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
// The diff hunk is the code the comment was left on. Without it the
// comment arrives without the context it was written against.
if (comment.diffHunk) {
const diffHunk = sanitizeContent(comment.diffHunk);
formatted += `\n Diff context:\n\`\`\`diff\n${diffHunk}\n\`\`\``;
}
return formatted;
}) })
.join("\n"); .join("\n");
if (comments) { if (comments) {
+1
View File
@@ -22,6 +22,7 @@ export type GitHubComment = {
export type GitHubReviewComment = GitHubComment & { export type GitHubReviewComment = GitHubComment & {
path: string; path: string;
line: number | null; line: number | null;
diffHunk?: string | null;
}; };
export type GitHubCommit = { export type GitHubCommit = {
+106
View File
@@ -508,6 +508,112 @@ describe("formatReviewComments", () => {
); );
}); });
test("includes the diff hunk as context when present", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "",
state: "COMMENTED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "This can overflow",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
diffHunk: "@@ -40,3 +40,3 @@\n-const a = 1;\n+const a = 2;",
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toContain("[Comment on src/index.ts:42]: This can overflow");
expect(result).toContain("Diff context:");
expect(result).toContain("@@ -40,3 +40,3 @@");
expect(result).toContain("+const a = 2;");
});
test("omits the diff context when the comment has no diff hunk", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "",
state: "COMMENTED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "No hunk here",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toContain("[Comment on src/index.ts:42]: No hunk here");
expect(result).not.toContain("Diff context:");
});
// GitHub returns line: null and diffHunk: "" for outdated comments whose
// line no longer exists in the diff (observed on anthropics/claude-code-action#1025).
test("omits the diff context for an outdated comment with an empty diff hunk", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "",
state: "COMMENTED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Outdated comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: null,
diffHunk: "",
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toContain("[Comment on src/index.ts:?]: Outdated comment");
expect(result).not.toContain("Diff context:");
expect(result).not.toContain("```diff");
});
test("formats review with only body (no comments) correctly", () => { test("formats review with only body (no comments) correctly", () => {
const reviewData = { const reviewData = {
nodes: [ nodes: [