| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | countReviewCommentContexts, |
| 5 | formatReviewCommentContext, |
| 6 | parseReviewCommentMessageSegments, |
| 7 | parseReviewInlineComments, |
| 8 | type ReviewCommentTarget, |
| 9 | } from "./reviewCommentSelection"; |
| 10 | |
| 11 | function 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 | |
| 45 | describe("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 | }); |