1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 countReviewCommentContexts,
5 formatReviewCommentContext,
6 parseReviewCommentMessageSegments,
7 parseReviewInlineComments,
8 type ReviewCommentTarget,
9} from "./reviewCommentSelection";
10
11function makeTarget(): ReviewCommentTarget {
12 return {
13 sectionId: "section-1",
14 sectionTitle: "Working tree",
15 filePath: "apps/demo/src/main.ts",
16 startIndex: 0,
17 endIndex: 1,
18 lines: [
19 {
20 kind: "line",
21 id: "line-1",
22 change: "delete",
23 oldLineNumber: 7,
24 newLineNumber: null,
25 content: "const retryLimit = 2;",
26 additionTokenIndex: null,
27 deletionTokenIndex: 0,
28 comparison: null,
29 },
30 {
31 kind: "line",
32 id: "line-2",
33 change: "add",
34 oldLineNumber: null,
35 newLineNumber: 7,
36 content: "const retryLimit = 4;",
37 additionTokenIndex: 0,
38 deletionTokenIndex: null,
39 comparison: null,
40 },
41 ],
42 };
43}
44
45describe("review comment serialization", () => {
46 it("keeps closing-tag text inside a chip label within a real review body", () => {
47 const body = "Before [</review_comment>](t3-context://v1/mention/context-1) after";
48 const serialized = `<review_comment sectionId="s" filePath="app.ts" startIndex="0" endIndex="0">${body}</review_comment>`;
49 const segments = parseReviewCommentMessageSegments(`${serialized} tail`);
50 expect(segments).toEqual([
51 { kind: "review-comment", comment: expect.objectContaining({ text: body }) },
52 { kind: "text", id: `review-comment-text:${serialized.length}`, text: " tail" },
53 ]);
54 });
55
56 it("keeps a closing tag inside a chip label out of the inline comment body", () => {
57 const body = "Before [</review_comment>](t3-context://v1/mention/context-1) after";
58 const serialized = `<review_comment sectionId="s" filePath="app.ts" startIndex="0" endIndex="0">${body}</review_comment>`;
59
60 expect(parseReviewInlineComments(serialized)).toEqual([
61 expect.objectContaining({ text: body }),
62 ]);
63 });
64
65 it("treats legacy markup inside a context label as opaque text", () => {
66 const text =
67 '[<review_comment sectionId="s" filePath="app.ts" startIndex="0" endIndex="0">Review this</review_comment>](t3-context://v1/mention/context-1)';
68 expect(parseReviewCommentMessageSegments(text)).toEqual([
69 { kind: "text", id: "review-comment-text:0", text },
70 ]);
71 });
72 it("preserves enough metadata for inline diff rendering", () => {
73 const serialized = formatReviewCommentContext(makeTarget(), "Please keep this configurable.");
74
75 expect(countReviewCommentContexts(serialized)).toBe(1);
76 expect(parseReviewInlineComments(serialized)).toEqual([
77 expect.objectContaining({
78 sectionId: "section-1",
79 sectionTitle: "Working tree",
80 filePath: "apps/demo/src/main.ts",
81 startIndex: 0,
82 endIndex: 1,
83 text: "Please keep this configurable.",
84 diff: expect.stringContaining("-const retryLimit = 2;"),
85 }),
86 ]);
87 });
88
89 it("splits chat text into review comment segments", () => {
90 const serialized = `Before\n${formatReviewCommentContext(makeTarget(), "Please keep this configurable.")}\nAfter`;
91 const segments = parseReviewCommentMessageSegments(serialized);
92
93 expect(segments).toHaveLength(3);
94 expect(segments[0]).toEqual(expect.objectContaining({ kind: "text", text: "Before\n" }));
95 expect(segments[1]).toEqual(
96 expect.objectContaining({
97 kind: "review-comment",
98 comment: expect.objectContaining({
99 filePath: "apps/demo/src/main.ts",
100 text: "Please keep this configurable.",
101 diff: expect.stringContaining("+const retryLimit = 4;"),
102 }),
103 }),
104 );
105 expect(segments[2]).toEqual(expect.objectContaining({ kind: "text", text: "\nAfter" }));
106 });
107
108 it("parses source-language review comments created by the web file viewer", () => {
109 const [segment] = parseReviewCommentMessageSegments(
110 [
111 '<review_comment sectionId="file:docs/plan.md" sectionTitle="File comment" filePath="docs/plan.md" startIndex="0" endIndex="1" rangeLabel="L1 to L2">',
112 "Clarify this.",
113 "```md",
114 "# Plan",
115 "- Step one",
116 "```",
117 "</review_comment>",
118 ].join("\n"),
119 );
120
121 expect(segment).toEqual(
122 expect.objectContaining({
123 kind: "review-comment",
124 comment: expect.objectContaining({
125 filePath: "docs/plan.md",
126 fenceLanguage: "md",
127 diff: "# Plan\n- Step one",
128 }),
129 }),
130 );
131 });
132
133 it("keeps fenced examples in comment prose separate from the context fence", () => {
134 const [segment] = parseReviewCommentMessageSegments(
135 [
136 '<review_comment sectionId="section-1" sectionTitle="Working tree" filePath="src/app.ts" startIndex="0" endIndex="0" rangeLabel="+1">',
137 "Try this:",
138 "```ts",
139 "const value = 1;",
140 "```",
141 "Then retry.",
142 "```diff",
143 "@@ -0,0 +1,1 @@",
144 "+one",
145 "```",
146 "</review_comment>",
147 ].join("\n"),
148 );
149
150 expect(segment).toEqual(
151 expect.objectContaining({
152 kind: "review-comment",
153 comment: expect.objectContaining({
154 text: ["Try this:", "```ts", "const value = 1;", "```", "Then retry."].join("\n"),
155 diff: "@@ -0,0 +1,1 @@\n+one",
156 }),
157 }),
158 );
159 });
160
161 it("round-trips greater-than signs in review attributes", () => {
162 const serialized = formatReviewCommentContext(
163 { ...makeTarget(), sectionTitle: "Changes > 5" },
164 "Check this.",
165 );
166 const [comment] = parseReviewInlineComments(serialized);
167
168 expect(serialized).toContain('sectionTitle="Changes &gt; 5"');
169 expect(comment?.sectionTitle).toBe("Changes > 5");
170 });
171});