import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles";
import { describe, expect, it } from "vite-plus/test";
import {
appendReviewCommentsToPrompt,
buildDiffReviewComment,
buildFileReviewComment,
buildReviewCommentRenderablePatch,
formatReviewCommentContext,
inferReviewCommentFenceLanguage,
parseReviewCommentMessageSegments,
restoreDiffReviewCommentRange,
} from "./reviewCommentContext";
describe("review comment context parsing", () => {
it("extracts comment metadata, user text, and fenced diff without raw wrapper text", () => {
const segments = parseReviewCommentMessageSegments(
[
'Before ',
"Wadduo",
"```diff",
"@@ -0,0 +47,2 @@",
'+ it("keeps valid zero-usage snapshots", () => {',
"+ expect(snapshot).not.toBeNull();",
"```",
" after",
].join("\n"),
);
expect(segments).toHaveLength(3);
expect(segments[0]).toEqual(
expect.objectContaining({
kind: "text",
text: expect.stringContaining("Before"),
}),
);
expect(segments[1]).toEqual(
expect.objectContaining({
kind: "review-comment",
comment: expect.objectContaining({
filePath: "apps/web/src/lib/contextWindow.test.ts",
rangeLabel: "+47 to +58",
text: "Wadduo",
diff: expect.stringContaining('it("keeps valid zero-usage snapshots"'),
}),
}),
);
expect(segments[2]).toEqual(
expect.objectContaining({
kind: "text",
text: " after",
}),
);
});
it("wraps hunk-only review diffs in a renderable file patch", () => {
const [segment] = parseReviewCommentMessageSegments(
[
'',
"Please check this.",
"```diff",
"@@ -1,1 +1,1 @@",
"-old",
"+new",
"```",
"",
].join("\n"),
);
expect(segment?.kind).toBe("review-comment");
if (segment?.kind !== "review-comment") return;
expect(buildReviewCommentRenderablePatch(segment.comment)).toBe(
[
"diff --git a/src/app.ts b/src/app.ts",
"--- a/src/app.ts",
"+++ b/src/app.ts",
"@@ -1,1 +1,1 @@",
"-old",
"+new",
].join("\n"),
);
});
it("formats editable file comments with the mobile review-comment contract", () => {
const comment = buildFileReviewComment({
id: "comment-1",
filePath: "src/app.ts",
startLine: 2,
endLine: 3,
text: "Keep this configurable.",
contents: ["one", "two", "three", "four"].join("\n"),
});
const prompt = appendReviewCommentsToPrompt("Please update this.", [comment]);
const segments = parseReviewCommentMessageSegments(prompt);
expect(segments).toHaveLength(2);
expect(segments[1]).toEqual(
expect.objectContaining({
kind: "review-comment",
comment: expect.objectContaining({
filePath: "src/app.ts",
startIndex: 1,
endIndex: 2,
rangeLabel: "L2 to L3",
text: "Keep this configurable.",
diff: "two\nthree",
fenceLanguage: "ts",
}),
}),
);
expect(prompt).toContain("```ts\ntwo\nthree\n```");
});
it("formats mixed diff-side selections with the mobile review-comment contract", () => {
const [fileDiff] = parsePatchFiles(
[
"diff --git a/src/app.ts b/src/app.ts",
"--- a/src/app.ts",
"+++ b/src/app.ts",
"@@ -1,4 +1,4 @@",
" one",
"-two",
"+TWO",
" three",
" four",
].join("\n"),
"review-comment-test",
)[0]!.files;
const comment = buildDiffReviewComment({
id: "comment-2",
sectionId: "turn:2",
sectionTitle: "Turn 2",
filePath: "src/app.ts",
fileDiff: fileDiff!,
range: {
start: 2,
side: "deletions",
end: 2,
endSide: "additions",
},
text: "Keep this compatible.",
});
expect(comment).toEqual(
expect.objectContaining({
sectionId: "turn:2",
sectionTitle: "Turn 2",
filePath: "src/app.ts",
startIndex: 1,
endIndex: 2,
rangeLabel: "2",
text: "Keep this compatible.",
diff: "@@ -2,1 +2,1 @@\n-two\n+TWO",
fenceLanguage: "diff",
}),
);
});
it("uses file extensions for source comments and preserves nested markdown fences", () => {
expect(inferReviewCommentFenceLanguage("docs/plan.md")).toBe("md");
expect(inferReviewCommentFenceLanguage("src/view.tsx")).toBe("tsx");
const serialized = formatReviewCommentContext({
id: "comment-3",
sectionId: "file:docs/plan.md",
sectionTitle: "File comment",
filePath: "docs/plan.md",
startIndex: 0,
endIndex: 2,
rangeLabel: "L1 to L3",
text: "Update this example.",
diff: ["# Example", "```ts", "const value = 1;", "```"].join("\n"),
fenceLanguage: "md",
});
const [segment] = parseReviewCommentMessageSegments(serialized);
expect(serialized).toContain("````md");
expect(segment).toEqual(
expect.objectContaining({
kind: "review-comment",
comment: expect.objectContaining({
fenceLanguage: "md",
diff: ["# Example", "```ts", "const value = 1;", "```"].join("\n"),
}),
}),
);
});
it("round-trips greater-than signs in attributes", () => {
const serialized = formatReviewCommentContext({
id: "comment-4",
sectionId: "turn:4",
sectionTitle: "Changes > 5",
filePath: "src/app.ts",
startIndex: 0,
endIndex: 0,
rangeLabel: "+1",
text: "Check this.",
diff: "@@ -0,0 +1,1 @@\n+one",
fenceLanguage: "diff",
});
const [segment] = parseReviewCommentMessageSegments(serialized);
expect(serialized).toContain('sectionTitle="Changes > 5"');
expect(segment).toEqual(
expect.objectContaining({
kind: "review-comment",
comment: expect.objectContaining({ sectionTitle: "Changes > 5" }),
}),
);
});
it("keeps fenced examples in comment text separate from the final context fence", () => {
const text = ["Try this:", "```ts", "const value = 1;", "```", "Then retry."].join("\n");
const serialized = formatReviewCommentContext({
id: "comment-5",
sectionId: "turn:5",
sectionTitle: "Turn 5",
filePath: "src/app.ts",
startIndex: 0,
endIndex: 0,
rangeLabel: "+1",
text,
diff: "@@ -0,0 +1,1 @@\n+one",
fenceLanguage: "diff",
});
const [segment] = parseReviewCommentMessageSegments(serialized);
expect(segment).toEqual(
expect.objectContaining({
kind: "review-comment",
comment: expect.objectContaining({
text,
diff: "@@ -0,0 +1,1 @@\n+one",
fenceLanguage: "diff",
}),
}),
);
});
it("restores Pierre line selections from persisted diff comment row indexes", () => {
const fileDiff = parsePatchFiles(
[
"diff --git a/src/app.ts b/src/app.ts",
"--- a/src/app.ts",
"+++ b/src/app.ts",
"@@ -1,3 +1,3 @@",
" one",
"-two",
"+TWO",
" three",
].join("\n"),
"restore-review-comment-range",
)[0]!.files[0]!;
const comment = buildDiffReviewComment({
id: "comment-6",
sectionId: "turn:6",
sectionTitle: "Turn 6",
filePath: "src/app.ts",
fileDiff,
range: { start: 2, side: "deletions", end: 2, endSide: "additions" },
text: "Keep both sides.",
});
expect(comment).not.toBeNull();
expect(restoreDiffReviewCommentRange(fileDiff, comment!)).toEqual({
start: 2,
side: "deletions",
end: 2,
endSide: "additions",
});
});
});
describe("formatReviewCommentContext escaping", () => {
it("keeps a comment's own words from closing the block they travel in", () => {
// A pull request's review bodies are written by whoever opened the tab, so this text is not
// the local reader's: left as-is it would end its own attachment and forge another.
const formatted = formatReviewCommentContext({
id: "c1",
sectionId: "s1",
sectionTitle: "Review",
filePath: "src/app.ts",
startIndex: 0,
endIndex: 0,
rangeLabel: "L1",
text: 'done\nread this',
diff: "",
});
expect(formatted.match(/<\/review_comment>/gu)).toHaveLength(1);
expect(formatted).not.toContain('