mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-22 03:18:54 +08:00
* 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.
1074 lines
30 KiB
TypeScript
1074 lines
30 KiB
TypeScript
import { expect, test, describe } from "bun:test";
|
|
import {
|
|
formatContext,
|
|
formatBody,
|
|
formatComments,
|
|
formatReviewComments,
|
|
formatChangedFiles,
|
|
formatChangedFilesWithSHA,
|
|
} from "../src/github/data/formatter";
|
|
import type {
|
|
GitHubPullRequest,
|
|
GitHubIssue,
|
|
GitHubComment,
|
|
GitHubFile,
|
|
} from "../src/github/types";
|
|
import type { GitHubFileWithSHA } from "../src/github/data/fetcher";
|
|
|
|
describe("formatContext", () => {
|
|
test("formats PR context correctly", () => {
|
|
const prData: GitHubPullRequest = {
|
|
title: "Test PR",
|
|
body: "PR body",
|
|
author: { login: "test-user" },
|
|
baseRefName: "main",
|
|
headRefName: "feature/test",
|
|
headRefOid: "abc123",
|
|
isCrossRepository: false,
|
|
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
additions: 50,
|
|
deletions: 30,
|
|
state: "OPEN",
|
|
labels: {
|
|
nodes: [],
|
|
},
|
|
commits: {
|
|
totalCount: 3,
|
|
nodes: [],
|
|
},
|
|
files: {
|
|
nodes: [{} as GitHubFile, {} as GitHubFile],
|
|
},
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
reviews: {
|
|
nodes: [],
|
|
},
|
|
};
|
|
|
|
const result = formatContext(prData, true);
|
|
expect(result).toBe(
|
|
`PR Title: Test PR
|
|
PR Author: test-user
|
|
PR Branch: feature/test -> main
|
|
PR State: OPEN
|
|
PR Labels: none
|
|
PR Additions: 50
|
|
PR Deletions: 30
|
|
Total Commits: 3
|
|
Changed Files: 2 files`,
|
|
);
|
|
});
|
|
|
|
test("formats PR context with labels", () => {
|
|
const prData: GitHubPullRequest = {
|
|
title: "Test PR",
|
|
body: "PR body",
|
|
author: { login: "test-user" },
|
|
baseRefName: "main",
|
|
headRefName: "feature/test",
|
|
headRefOid: "abc123",
|
|
isCrossRepository: false,
|
|
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
additions: 50,
|
|
deletions: 30,
|
|
state: "OPEN",
|
|
labels: {
|
|
nodes: [{ name: "bug" }, { name: "enhancement" }],
|
|
},
|
|
commits: {
|
|
totalCount: 3,
|
|
nodes: [],
|
|
},
|
|
files: {
|
|
nodes: [{} as GitHubFile, {} as GitHubFile],
|
|
},
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
reviews: {
|
|
nodes: [],
|
|
},
|
|
};
|
|
|
|
const result = formatContext(prData, true);
|
|
expect(result).toBe(
|
|
`PR Title: Test PR
|
|
PR Author: test-user
|
|
PR Branch: feature/test -> main
|
|
PR State: OPEN
|
|
PR Labels: bug, enhancement
|
|
PR Additions: 50
|
|
PR Deletions: 30
|
|
Total Commits: 3
|
|
Changed Files: 2 files`,
|
|
);
|
|
});
|
|
|
|
test("renders an unknown file count when GraphQL returns null files (very large PR)", () => {
|
|
// GitHub declines to compute the diff for very large PRs and returns
|
|
// `files: null`. `changedFiles` is misreported as 0 in that case, so the
|
|
// count must render as unavailable rather than "0 files".
|
|
const prData: GitHubPullRequest = {
|
|
title: "Very large PR",
|
|
body: "PR body",
|
|
author: { login: "test-user" },
|
|
baseRefName: "main",
|
|
headRefName: "feature/test",
|
|
headRefOid: "abc123",
|
|
isCrossRepository: false,
|
|
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
additions: 50,
|
|
deletions: 30,
|
|
state: "OPEN",
|
|
labels: {
|
|
nodes: [],
|
|
},
|
|
commits: {
|
|
totalCount: 3,
|
|
nodes: [],
|
|
},
|
|
files: null,
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
reviews: {
|
|
nodes: [],
|
|
},
|
|
};
|
|
|
|
const result = formatContext(prData, true);
|
|
expect(result).toContain("Changed Files: unknown (file list unavailable)");
|
|
});
|
|
|
|
test("formats Issue context correctly", () => {
|
|
const issueData: GitHubIssue = {
|
|
title: "Test Issue",
|
|
body: "Issue body",
|
|
author: { login: "test-user" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
state: "OPEN",
|
|
labels: {
|
|
nodes: [],
|
|
},
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
};
|
|
|
|
const result = formatContext(issueData, false);
|
|
expect(result).toBe(
|
|
`Issue Title: Test Issue
|
|
Issue Author: test-user
|
|
Issue State: OPEN
|
|
Issue Labels: none`,
|
|
);
|
|
});
|
|
|
|
test("formats Issue context with labels", () => {
|
|
const issueData: GitHubIssue = {
|
|
title: "Test Issue",
|
|
body: "Issue body",
|
|
author: { login: "test-user" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
state: "OPEN",
|
|
labels: {
|
|
nodes: [
|
|
{ name: "architecture" },
|
|
{ name: "agent-sdk" },
|
|
{ name: "drift:functional" },
|
|
],
|
|
},
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
};
|
|
|
|
const result = formatContext(issueData, false);
|
|
expect(result).toBe(
|
|
`Issue Title: Test Issue
|
|
Issue Author: test-user
|
|
Issue State: OPEN
|
|
Issue Labels: architecture, agent-sdk, drift:functional`,
|
|
);
|
|
});
|
|
|
|
test("renders a deleted (null-author) issue author as 'ghost'", () => {
|
|
const issueData: GitHubIssue = {
|
|
title: "Test Issue",
|
|
body: "Issue body",
|
|
author: null,
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
state: "OPEN",
|
|
labels: { nodes: [] },
|
|
comments: { nodes: [] },
|
|
};
|
|
|
|
const result = formatContext(issueData, false);
|
|
expect(result).toContain("Issue Author: ghost");
|
|
});
|
|
});
|
|
|
|
describe("formatBody", () => {
|
|
test("replaces image URLs with local paths", () => {
|
|
const body = `Here is some text with an image: 
|
|
|
|
And another one: 
|
|
|
|
Some more text.`;
|
|
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/test-image.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
[
|
|
"https://github.com/user-attachments/assets/another-image.jpg",
|
|
"/tmp/github-images/image-1234-1.jpg",
|
|
],
|
|
]);
|
|
|
|
const result = formatBody(body, imageUrlMap);
|
|
expect(result)
|
|
.toBe(`Here is some text with an image: 
|
|
|
|
And another one: 
|
|
|
|
Some more text.`);
|
|
});
|
|
|
|
test("handles empty image map", () => {
|
|
const body = "No images here";
|
|
const imageUrlMap = new Map<string, string>();
|
|
|
|
const result = formatBody(body, imageUrlMap);
|
|
expect(result).toBe("No images here");
|
|
});
|
|
|
|
test("preserves body when no images match", () => {
|
|
const body = "";
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/different.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
]);
|
|
|
|
const result = formatBody(body, imageUrlMap);
|
|
expect(result).toBe("");
|
|
});
|
|
|
|
test("handles multiple occurrences of same image", () => {
|
|
const body = `First: 
|
|
Second: `;
|
|
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/test.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
]);
|
|
|
|
const result = formatBody(body, imageUrlMap);
|
|
expect(result).toBe(`First: 
|
|
Second: `);
|
|
});
|
|
});
|
|
|
|
describe("formatComments", () => {
|
|
test("formats comments correctly", () => {
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "First comment",
|
|
author: { login: "user1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
},
|
|
{
|
|
id: "2",
|
|
databaseId: "100002",
|
|
body: "Second comment",
|
|
author: { login: "user2" },
|
|
createdAt: "2023-01-02T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const result = formatComments(comments);
|
|
expect(result).toBe(
|
|
`[user1 at 2023-01-01T00:00:00Z]: First comment\n\n[user2 at 2023-01-02T00:00:00Z]: Second comment`,
|
|
);
|
|
});
|
|
|
|
test("renders deleted (null-author) comments as 'ghost'", () => {
|
|
// GitHub returns author: null for comments from deleted accounts.
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "From a deleted account",
|
|
author: null,
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const result = formatComments(comments);
|
|
expect(result).toBe(
|
|
"[ghost at 2023-01-01T00:00:00Z]: From a deleted account",
|
|
);
|
|
});
|
|
|
|
test("returns empty string for empty comments array", () => {
|
|
const result = formatComments([]);
|
|
expect(result).toBe("");
|
|
});
|
|
|
|
test("replaces image URLs in comments", () => {
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "Check out this screenshot: ",
|
|
author: { login: "user1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
},
|
|
{
|
|
id: "2",
|
|
databaseId: "100002",
|
|
body: "Here's another image: ",
|
|
author: { login: "user2" },
|
|
createdAt: "2023-01-02T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/screenshot.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
[
|
|
"https://github.com/user-attachments/assets/bug-report.jpg",
|
|
"/tmp/github-images/image-1234-1.jpg",
|
|
],
|
|
]);
|
|
|
|
const result = formatComments(comments, imageUrlMap);
|
|
expect(result).toBe(
|
|
`[user1 at 2023-01-01T00:00:00Z]: Check out this screenshot: \n\n[user2 at 2023-01-02T00:00:00Z]: Here's another image: `,
|
|
);
|
|
});
|
|
|
|
test("handles comments with multiple images", () => {
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "Two images:  and ",
|
|
author: { login: "user1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/first.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
[
|
|
"https://github.com/user-attachments/assets/second.png",
|
|
"/tmp/github-images/image-1234-1.png",
|
|
],
|
|
]);
|
|
|
|
const result = formatComments(comments, imageUrlMap);
|
|
expect(result).toBe(
|
|
`[user1 at 2023-01-01T00:00:00Z]: Two images:  and `,
|
|
);
|
|
});
|
|
|
|
test("preserves comments when imageUrlMap is undefined", () => {
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "Image: ",
|
|
author: { login: "user1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const result = formatComments(comments);
|
|
expect(result).toBe(
|
|
`[user1 at 2023-01-01T00:00:00Z]: Image: `,
|
|
);
|
|
});
|
|
|
|
test("filters out minimized comments", () => {
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "Normal comment",
|
|
author: { login: "user1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
isMinimized: false,
|
|
},
|
|
{
|
|
id: "2",
|
|
databaseId: "100002",
|
|
body: "Minimized comment",
|
|
author: { login: "user2" },
|
|
createdAt: "2023-01-02T00:00:00Z",
|
|
isMinimized: true,
|
|
},
|
|
{
|
|
id: "3",
|
|
databaseId: "100003",
|
|
body: "Another normal comment",
|
|
author: { login: "user3" },
|
|
createdAt: "2023-01-03T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const result = formatComments(comments);
|
|
expect(result).toBe(
|
|
`[user1 at 2023-01-01T00:00:00Z]: Normal comment\n\n[user3 at 2023-01-03T00:00:00Z]: Another normal comment`,
|
|
);
|
|
});
|
|
|
|
test("returns empty string when all comments are minimized", () => {
|
|
const comments: GitHubComment[] = [
|
|
{
|
|
id: "1",
|
|
databaseId: "100001",
|
|
body: "Minimized comment 1",
|
|
author: { login: "user1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
isMinimized: true,
|
|
},
|
|
{
|
|
id: "2",
|
|
databaseId: "100002",
|
|
body: "Minimized comment 2",
|
|
author: { login: "user2" },
|
|
createdAt: "2023-01-02T00:00:00Z",
|
|
isMinimized: true,
|
|
},
|
|
];
|
|
|
|
const result = formatComments(comments);
|
|
expect(result).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("formatReviewComments", () => {
|
|
test("formats review with body and comments correctly", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "This is a great PR! LGTM.",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Nice implementation",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/index.ts",
|
|
line: 42,
|
|
},
|
|
{
|
|
id: "comment2",
|
|
databaseId: "200002",
|
|
body: "Consider adding error handling",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/utils.ts",
|
|
line: null,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nThis is a great PR! LGTM.\n [Comment on src/index.ts:42]: Nice implementation\n [Comment on src/utils.ts:?]: Consider adding error handling`,
|
|
);
|
|
});
|
|
|
|
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", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300002",
|
|
author: { login: "reviewer1" },
|
|
body: "Looks good to me!",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nLooks good to me!`,
|
|
);
|
|
});
|
|
|
|
test("formats review without body correctly", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300003",
|
|
author: { login: "reviewer1" },
|
|
body: "",
|
|
state: "COMMENTED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200003",
|
|
body: "Small suggestion here",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/main.ts",
|
|
line: 15,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: COMMENTED\n [Comment on src/main.ts:15]: Small suggestion here`,
|
|
);
|
|
});
|
|
|
|
test("renders deleted (null-author) reviews as 'ghost'", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300099",
|
|
author: null,
|
|
body: "Left before deleting the account",
|
|
state: "COMMENTED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by ghost at 2023-01-01T00:00:00Z]: COMMENTED\nLeft before deleting the account`,
|
|
);
|
|
});
|
|
|
|
test("formats multiple reviews correctly", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300004",
|
|
author: { login: "reviewer1" },
|
|
body: "Needs changes",
|
|
state: "CHANGES_REQUESTED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
},
|
|
{
|
|
id: "review2",
|
|
databaseId: "300005",
|
|
author: { login: "reviewer2" },
|
|
body: "LGTM",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-02T00:00:00Z",
|
|
comments: {
|
|
nodes: [],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: CHANGES_REQUESTED\nNeeds changes\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: APPROVED\nLGTM`,
|
|
);
|
|
});
|
|
|
|
test("returns empty string for null reviewData", () => {
|
|
const result = formatReviewComments(null);
|
|
expect(result).toBe("");
|
|
});
|
|
|
|
test("returns empty string for empty reviewData", () => {
|
|
const result = formatReviewComments({ nodes: [] });
|
|
expect(result).toBe("");
|
|
});
|
|
|
|
test("replaces image URLs in review comments", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "Review with image: ",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Comment with image: ",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/index.ts",
|
|
line: 42,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/review.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
[
|
|
"https://github.com/user-attachments/assets/comment.png",
|
|
"/tmp/github-images/image-1234-1.png",
|
|
],
|
|
]);
|
|
|
|
const result = formatReviewComments(reviewData, imageUrlMap);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with image: \n [Comment on src/index.ts:42]: Comment with image: `,
|
|
);
|
|
});
|
|
|
|
test("handles multiple images in review comments", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "Good work",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Two issues:  and ",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/main.ts",
|
|
line: 15,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const imageUrlMap = new Map([
|
|
[
|
|
"https://github.com/user-attachments/assets/issue1.png",
|
|
"/tmp/github-images/image-1234-0.png",
|
|
],
|
|
[
|
|
"https://github.com/user-attachments/assets/issue2.png",
|
|
"/tmp/github-images/image-1234-1.png",
|
|
],
|
|
]);
|
|
|
|
const result = formatReviewComments(reviewData, imageUrlMap);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nGood work\n [Comment on src/main.ts:15]: Two issues:  and `,
|
|
);
|
|
});
|
|
|
|
test("preserves review comments when imageUrlMap is undefined", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "Review body",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Image: ",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/index.ts",
|
|
line: 42,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: `,
|
|
);
|
|
});
|
|
|
|
test("filters out minimized review comments", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "Review with mixed comments",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Normal review comment",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/index.ts",
|
|
line: 42,
|
|
isMinimized: false,
|
|
},
|
|
{
|
|
id: "comment2",
|
|
databaseId: "200002",
|
|
body: "Minimized review comment",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/utils.ts",
|
|
line: 15,
|
|
isMinimized: true,
|
|
},
|
|
{
|
|
id: "comment3",
|
|
databaseId: "200003",
|
|
body: "Another normal comment",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/main.ts",
|
|
line: 10,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with mixed comments\n [Comment on src/index.ts:42]: Normal review comment\n [Comment on src/main.ts:10]: Another normal comment`,
|
|
);
|
|
});
|
|
|
|
test("returns review with only body when all review comments are minimized", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "Review body only",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Minimized comment 1",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/index.ts",
|
|
line: 42,
|
|
isMinimized: true,
|
|
},
|
|
{
|
|
id: "comment2",
|
|
databaseId: "200002",
|
|
body: "Minimized comment 2",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/utils.ts",
|
|
line: 15,
|
|
isMinimized: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body only`,
|
|
);
|
|
});
|
|
|
|
test("handles multiple reviews with mixed minimized comments", () => {
|
|
const reviewData = {
|
|
nodes: [
|
|
{
|
|
id: "review1",
|
|
databaseId: "300001",
|
|
author: { login: "reviewer1" },
|
|
body: "First review",
|
|
state: "APPROVED",
|
|
submittedAt: "2023-01-01T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment1",
|
|
databaseId: "200001",
|
|
body: "Good comment",
|
|
author: { login: "reviewer1" },
|
|
createdAt: "2023-01-01T00:00:00Z",
|
|
path: "src/index.ts",
|
|
line: 42,
|
|
isMinimized: false,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: "review2",
|
|
databaseId: "300002",
|
|
author: { login: "reviewer2" },
|
|
body: "Second review",
|
|
state: "COMMENTED",
|
|
submittedAt: "2023-01-02T00:00:00Z",
|
|
comments: {
|
|
nodes: [
|
|
{
|
|
id: "comment2",
|
|
databaseId: "200002",
|
|
body: "Spam comment",
|
|
author: { login: "reviewer2" },
|
|
createdAt: "2023-01-02T00:00:00Z",
|
|
path: "src/utils.ts",
|
|
line: 15,
|
|
isMinimized: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = formatReviewComments(reviewData);
|
|
expect(result).toBe(
|
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nFirst review\n [Comment on src/index.ts:42]: Good comment\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: COMMENTED\nSecond review`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("formatChangedFiles", () => {
|
|
test("formats changed files correctly", () => {
|
|
const files: GitHubFile[] = [
|
|
{
|
|
path: "src/index.ts",
|
|
additions: 10,
|
|
deletions: 5,
|
|
changeType: "MODIFIED",
|
|
},
|
|
{
|
|
path: "src/utils.ts",
|
|
additions: 20,
|
|
deletions: 0,
|
|
changeType: "ADDED",
|
|
},
|
|
];
|
|
|
|
const result = formatChangedFiles(files);
|
|
expect(result).toBe(
|
|
`- src/index.ts (MODIFIED) +10/-5\n- src/utils.ts (ADDED) +20/-0`,
|
|
);
|
|
});
|
|
|
|
test("returns empty string for empty files array", () => {
|
|
const result = formatChangedFiles([]);
|
|
expect(result).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("formatChangedFilesWithSHA", () => {
|
|
test("formats changed files with SHA correctly", () => {
|
|
const files: GitHubFileWithSHA[] = [
|
|
{
|
|
path: "src/index.ts",
|
|
additions: 10,
|
|
deletions: 5,
|
|
changeType: "MODIFIED",
|
|
sha: "abc123",
|
|
},
|
|
{
|
|
path: "src/utils.ts",
|
|
additions: 20,
|
|
deletions: 0,
|
|
changeType: "ADDED",
|
|
sha: "def456",
|
|
},
|
|
];
|
|
|
|
const result = formatChangedFilesWithSHA(files);
|
|
expect(result).toBe(
|
|
`- src/index.ts (MODIFIED) +10/-5 SHA: abc123\n- src/utils.ts (ADDED) +20/-0 SHA: def456`,
|
|
);
|
|
});
|
|
|
|
test("returns empty string for empty files array", () => {
|
|
const result = formatChangedFilesWithSHA([]);
|
|
expect(result).toBe("");
|
|
});
|
|
});
|