Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

code-suggestions.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

code-suggestions.test.tsBlame313 lines · 1 contributor
7c0203eClaude1/**
2 * Block J22 — Code review suggestion blocks. Pure extract + apply tests.
3 */
4
5import { describe, it, expect } from "bun:test";
6import {
7 detectLineEnding,
8 splitLines,
9 extractSuggestions,
10 applySuggestionToContent,
11 applyNthSuggestion,
12 __internal,
13} from "../lib/code-suggestions";
14
15describe("code-suggestions — detectLineEnding", () => {
16 it("returns CRLF for CRLF content", () => {
17 expect(detectLineEnding("a\r\nb\r\n")).toBe("\r\n");
18 });
19 it("returns LF for LF content", () => {
20 expect(detectLineEnding("a\nb\n")).toBe("\n");
21 });
22 it("returns LF for empty", () => {
23 expect(detectLineEnding("")).toBe("\n");
24 });
25 it("returns CRLF if ANY CRLF present", () => {
26 expect(detectLineEnding("a\nb\r\nc")).toBe("\r\n");
27 });
28});
29
30describe("code-suggestions — extractSuggestions", () => {
31 it("returns [] for empty or non-string input", () => {
32 expect(extractSuggestions("")).toEqual([]);
33 expect(extractSuggestions(null as unknown as string)).toEqual([]);
34 expect(extractSuggestions(undefined as unknown as string)).toEqual([]);
35 });
36
37 it("extracts a single single-line suggestion", () => {
38 const body = "LGTM but\n\n```suggestion\nconst x = 1;\n```\n\nthoughts?";
39 const out = extractSuggestions(body);
40 expect(out).toHaveLength(1);
41 expect(out[0].content).toBe("const x = 1;");
42 expect(out[0].index).toBe(0);
43 });
44
45 it("extracts multi-line suggestions preserving internal newlines", () => {
46 const body = "```suggestion\nline1\nline2\nline3\n```\n";
47 const out = extractSuggestions(body);
48 expect(out).toHaveLength(1);
49 expect(out[0].content).toBe("line1\nline2\nline3");
50 });
51
52 it("captures empty suggestions (intent: delete the line)", () => {
53 const body = "```suggestion\n```\n";
54 const out = extractSuggestions(body);
55 expect(out).toHaveLength(1);
56 expect(out[0].content).toBe("");
57 });
58
59 it("skips fences whose info-string isn't `suggestion`", () => {
60 const body = "```js\nconst x = 1;\n```\n";
61 expect(extractSuggestions(body)).toEqual([]);
62 });
63
64 it("extracts multiple suggestions in order", () => {
65 const body =
66 "First:\n```suggestion\nfoo\n```\n\nSecond:\n```suggestion\nbar\nbaz\n```\n";
67 const out = extractSuggestions(body);
68 expect(out).toHaveLength(2);
69 expect(out[0].content).toBe("foo");
70 expect(out[1].content).toBe("bar\nbaz");
71 expect(out[0].index).toBe(0);
72 expect(out[1].index).toBe(1);
73 });
74
75 it("skips unterminated fences", () => {
76 const body = "```suggestion\nconst x = 1;";
77 expect(extractSuggestions(body)).toEqual([]);
78 });
79
80 it("tolerates 4+ backtick fences (for suggestions that contain ```)", () => {
81 const body =
82 "````suggestion\nconst s = `template ${with} backticks`;\n````\n";
83 const out = extractSuggestions(body);
84 expect(out).toHaveLength(1);
85 expect(out[0].content).toBe(
86 "const s = `template ${with} backticks`;"
87 );
88 });
89
90 it("ignores trailing info after `suggestion`", () => {
91 const body = "```suggestion block\nfoo\n```\n";
92 expect(extractSuggestions(body)).toHaveLength(1);
93 });
94
95 it("is case-sensitive on the language token (GitHub parity)", () => {
96 // GitHub's renderer is case-sensitive. This keeps our parser
97 // predictable — bump to case-insensitive when GitHub relaxes.
98 const body = "```Suggestion\nfoo\n```\n";
99 expect(extractSuggestions(body)).toEqual([]);
100 });
101});
102
103describe("code-suggestions — applySuggestionToContent", () => {
104 const content = "line one\nline two\nline three\n";
105
106 it("replaces a single line (1-indexed)", () => {
107 const res = applySuggestionToContent({
108 content,
109 startLine: 2,
110 endLine: 2,
111 suggestion: "TWO!",
112 });
113 expect(res.ok).toBe(true);
114 expect(res.content).toBe("line one\nTWO!\nline three\n");
115 });
116
117 it("replaces with multiple lines", () => {
118 const res = applySuggestionToContent({
119 content,
120 startLine: 2,
121 endLine: 2,
122 suggestion: "a\nb\nc",
123 });
124 expect(res.ok).toBe(true);
125 expect(res.content).toBe("line one\na\nb\nc\nline three\n");
126 });
127
128 it("replaces a multi-line range", () => {
129 const res = applySuggestionToContent({
130 content,
131 startLine: 1,
132 endLine: 2,
133 suggestion: "merged",
134 });
135 expect(res.ok).toBe(true);
136 expect(res.content).toBe("merged\nline three\n");
137 });
138
139 it("preserves CRLF line endings", () => {
140 const crlf = "a\r\nb\r\nc\r\n";
141 const res = applySuggestionToContent({
142 content: crlf,
143 startLine: 2,
144 endLine: 2,
145 suggestion: "B",
146 });
147 expect(res.ok).toBe(true);
148 expect(res.content).toBe("a\r\nB\r\nc\r\n");
149 });
150
151 it("preserves no-trailing-newline files", () => {
152 const res = applySuggestionToContent({
153 content: "a\nb\nc",
154 startLine: 3,
155 endLine: 3,
156 suggestion: "C",
157 });
158 expect(res.ok).toBe(true);
159 expect(res.content).toBe("a\nb\nC");
160 });
161
162 it("strips trailing newline from the suggestion", () => {
163 const res = applySuggestionToContent({
164 content: "a\nb\n",
165 startLine: 1,
166 endLine: 1,
167 suggestion: "A\n",
168 });
169 expect(res.ok).toBe(true);
170 expect(res.content).toBe("A\nb\n");
171 });
172
173 it("rejects bad ranges", () => {
174 const r1 = applySuggestionToContent({
175 content,
176 startLine: 0,
177 endLine: 1,
178 suggestion: "x",
179 });
180 expect(r1).toEqual({ ok: false, reason: "bad_range" });
181 const r2 = applySuggestionToContent({
182 content,
183 startLine: 2,
184 endLine: 1,
185 suggestion: "x",
186 });
187 expect(r2).toEqual({ ok: false, reason: "bad_range" });
188 });
189
190 it("rejects out-of-bounds line numbers", () => {
191 const r = applySuggestionToContent({
192 content,
193 startLine: 5,
194 endLine: 5,
195 suggestion: "x",
196 });
197 expect(r).toEqual({ ok: false, reason: "line_out_of_bounds" });
198 });
199
200 it("returns no_change when the suggestion matches existing content", () => {
201 const r = applySuggestionToContent({
202 content,
203 startLine: 1,
204 endLine: 1,
205 suggestion: "line one",
206 });
207 expect(r).toEqual({ ok: false, reason: "no_change" });
208 });
209
210 it("empty suggestion deletes the line", () => {
211 const r = applySuggestionToContent({
212 content,
213 startLine: 2,
214 endLine: 2,
215 suggestion: "",
216 });
217 expect(r.ok).toBe(true);
218 expect(r.content).toBe("line one\n\nline three\n");
219 });
220
221 it("rejects non-string content", () => {
222 const r = applySuggestionToContent({
223 content: undefined as unknown as string,
224 startLine: 1,
225 endLine: 1,
226 suggestion: "x",
227 });
228 expect(r.ok).toBe(false);
229 });
230});
231
232describe("code-suggestions — applyNthSuggestion", () => {
233 const body =
234 "First:\n```suggestion\nalpha\n```\n\nSecond:\n```suggestion\nbeta\n```\n";
235
236 it("applies the 0th block by default", () => {
237 const r = applyNthSuggestion(body, 0, {
238 content: "x\n",
239 startLine: 1,
240 endLine: 1,
241 });
242 expect(r.ok).toBe(true);
243 if (r.ok) expect(r.content).toBe("alpha\n");
244 });
245
246 it("applies the Nth block", () => {
247 const r = applyNthSuggestion(body, 1, {
248 content: "x\n",
249 startLine: 1,
250 endLine: 1,
251 });
252 expect(r.ok).toBe(true);
253 if (r.ok) expect(r.content).toBe("beta\n");
254 });
255
256 it("returns not_found for an out-of-range index", () => {
257 const r = applyNthSuggestion(body, 99, {
258 content: "x\n",
259 startLine: 1,
260 endLine: 1,
261 });
262 expect(r).toEqual({ ok: false, reason: "not_found" });
263 });
264
265 it("returns not_found for a negative index", () => {
266 const r = applyNthSuggestion(body, -1, {
267 content: "x\n",
268 startLine: 1,
269 endLine: 1,
270 });
271 expect(r).toEqual({ ok: false, reason: "not_found" });
272 });
273});
274
275describe("code-suggestions — __internal parity", () => {
276 it("re-exports helpers", () => {
277 expect(__internal.extractSuggestions).toBe(extractSuggestions);
278 expect(__internal.applySuggestionToContent).toBe(applySuggestionToContent);
279 expect(__internal.detectLineEnding).toBe(detectLineEnding);
280 expect(__internal.splitLines).toBe(splitLines);
281 expect(__internal.applyNthSuggestion).toBe(applyNthSuggestion);
282 });
283});
284
285describe("code-suggestions — splitLines", () => {
286 it("handles LF + CRLF correctly", () => {
287 expect(splitLines("a\nb\nc")).toEqual(["a", "b", "c"]);
288 expect(splitLines("a\r\nb\r\nc")).toEqual(["a", "b", "c"]);
289 });
290});
291
292describe("code-suggestions — routes", () => {
293 // Import the app lazily so the pure tests above don't boot Hono.
294 it("POST apply-suggestion 401s for unauthenticated users", async () => {
295 const { default: app } = await import("../app");
296 const res = await app.request(
297 "/alice/repo/pulls/1/comments/00000000-0000-0000-0000-000000000000/apply-suggestion",
298 { method: "POST" }
299 );
300 // requireAuth redirects web requests to /login.
301 expect([302, 401]).toContain(res.status);
302 });
303
304 it("invalid PR number returns 4xx", async () => {
305 const { default: app } = await import("../app");
306 const res = await app.request(
307 "/alice/repo/pulls/notanum/comments/00000000-0000-0000-0000-000000000000/apply-suggestion",
308 { method: "POST" }
309 );
310 // Unauth first; if somehow auth'd, would be 400. Both are acceptable.
311 expect([302, 400, 401, 404]).toContain(res.status);
312 });
313});