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

ai-review.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.

ai-review.test.tsBlame184 lines · 2 contributors
fd87bb9Claude1/**
2 * Tests for src/lib/ai-review.ts.
3 *
4 * The Anthropic-calling path (reviewDiff) and the DB-touching paths
5 * (triggerAiReview's idempotency probe + insert) require external
6 * services we don't stand up in unit tests. We therefore cover the
7 * pure invariants:
8 *
9 * - The marker constant is stable so future searches keep working.
10 * - isAiReviewEnabled is a boolean reflecting config.anthropicApiKey.
11 * - triggerAiReview is a no-op (resolves cleanly) when the API key
12 * is absent — this is the documented graceful-degrade contract.
13 * - triggerAiReview never throws even when called with garbage.
14 * - The internal __test helpers are exported and shaped correctly.
15 */
16
17import { describe, it, expect } from "bun:test";
18import {
19 AI_REVIEW_MARKER,
20 isAiReviewEnabled,
21 triggerAiReview,
c166384ccantynz-alt22 computeAiReviewApproval,
fd87bb9Claude23 __test,
24} from "../lib/ai-review";
25
26describe("AI_REVIEW_MARKER", () => {
27 it("is the documented stable string", () => {
28 // Important: any change to this string is a breaking change for
29 // idempotency. New version → write a migration to back-fill old
30 // markers so older AI summaries still suppress duplicates.
31 expect(AI_REVIEW_MARKER).toBe("<!-- gluecron-ai-review:summary -->");
32 });
33
34 it("is an HTML comment so it doesn't render in markdown", () => {
35 expect(AI_REVIEW_MARKER.startsWith("<!--")).toBe(true);
36 expect(AI_REVIEW_MARKER.endsWith("-->")).toBe(true);
37 });
38});
39
40describe("isAiReviewEnabled", () => {
41 it("returns a boolean", () => {
42 const v = isAiReviewEnabled();
43 expect(typeof v).toBe("boolean");
44 });
45});
46
47describe("triggerAiReview — graceful degrade + crash-free", () => {
48 // Note: in the test sandbox ANTHROPIC_API_KEY is unset, so the
49 // function should resolve immediately without touching the DB. We
50 // also pass garbage repo names + nonexistent PR ids to confirm the
51 // overall try/catch holds.
52 it("resolves without throwing when API key is absent", async () => {
53 const before = process.env.ANTHROPIC_API_KEY;
54 delete process.env.ANTHROPIC_API_KEY;
55 try {
56 await triggerAiReview(
57 "alice",
58 "demo",
59 "00000000-0000-0000-0000-000000000000",
60 "Test PR",
61 "Body",
62 "main",
63 "feature"
64 );
65 } finally {
66 if (before) process.env.ANTHROPIC_API_KEY = before;
67 }
68 expect(true).toBe(true);
69 });
70
71 it("never throws even with invalid inputs", async () => {
72 let threw = false;
73 try {
74 await triggerAiReview(
75 "",
76 "",
77 "not-a-uuid",
78 "",
79 "",
80 "",
81 ""
82 );
83 } catch {
84 threw = true;
85 }
86 expect(threw).toBe(false);
87 });
88
89 it("survives an unknown branch combination without throwing", async () => {
90 let threw = false;
91 try {
92 await triggerAiReview(
93 "definitely-not-a-real-owner",
94 "definitely-not-a-real-repo",
95 "00000000-0000-0000-0000-000000000000",
96 "Title",
97 "Body",
98 "definitely-not-a-real-base",
99 "definitely-not-a-real-head"
100 );
101 } catch {
102 threw = true;
103 }
104 expect(threw).toBe(false);
105 });
106});
107
c166384ccantynz-alt108describe("computeAiReviewApproval — merge-gate decision", () => {
109 // Regression coverage for a real bug: three call sites (the MCP merge
110 // tool, the PR-page gate preview, and the web merge handler) each had
111 // their own ad-hoc heuristic for "did AI review approve this", and none
112 // of them matched the literal verdict strings triggerAiReview actually
113 // writes -- so the MCP path in particular hard-blocked every merge the
114 // instant AI review ran at all, clean or not. Reproduced live via
115 // scripts/agent-journey.ts against production.
116 const marker = AI_REVIEW_MARKER;
117
118 it("approves when there are no AI comments at all", () => {
119 expect(computeAiReviewApproval([])).toBe(true);
120 });
121
122 it("approves on the real clean-verdict string", () => {
123 const body = `${marker}\n## AI Code Review\n\n**AI review:** no blocking issues found.\n\nLooks good.`;
124 expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(true);
125 });
126
127 it("blocks on the real flagged-items verdict string", () => {
128 const body = `${marker}\n## AI Code Review\n\n**AI review:** flagged 2 item(s) for human attention.\n\nSee inline comments.`;
129 expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(false);
130 });
131
132 it("fails open on 'AI review unavailable' (provider error, e.g. no API credit)", () => {
133 const body = `${marker}\n## AI review unavailable\n\nThe AI review attempt failed: 400 credit balance too low. The PR is otherwise unchanged.`;
134 expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(true);
135 });
136
137 it("fails open on 'AI review skipped' (quota exhausted)", () => {
138 const body = `${marker}\n## AI review skipped\n\nYour monthly AI token budget has been reached.`;
139 expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(true);
140 });
141
142 it("ignores non-summary AI comments (e.g. inline findings, triage) with no marker", () => {
143 const inlineFinding = { body: "This line looks risky.", createdAt: new Date() };
144 expect(computeAiReviewApproval([inlineFinding])).toBe(true);
145 });
146
147 it("uses the most recent summary when a PR was re-reviewed after a push", () => {
148 const older = {
149 body: `${marker}\n**AI review:** flagged 1 item(s) for human attention.`,
150 createdAt: new Date("2026-01-01T00:00:00Z"),
151 };
152 const newer = {
153 body: `${marker}\n**AI review:** no blocking issues found.`,
154 createdAt: new Date("2026-01-02T00:00:00Z"),
155 };
156 expect(computeAiReviewApproval([older, newer])).toBe(true);
157 expect(computeAiReviewApproval([newer, older])).toBe(true);
158 });
159});
160
fd87bb9Claude161describe("__test internals", () => {
162 it("exports diffBetweenBranches and alreadyReviewed", () => {
163 expect(typeof __test.diffBetweenBranches).toBe("function");
164 expect(typeof __test.alreadyReviewed).toBe("function");
165 });
166
167 it("diffBetweenBranches returns '' for a nonexistent repo", async () => {
168 const out = await __test.diffBetweenBranches(
169 "definitely-not-a-real-owner",
170 "definitely-not-a-real-repo",
171 "main",
172 "feature"
173 );
174 expect(out).toBe("");
175 });
176
177 it("alreadyReviewed returns false for an unknown PR id (fail-open)", async () => {
8ed88f2ccantynz-alt178 const out = await __test.alreadyReviewed(
179 "00000000-0000-0000-0000-000000000000",
180 "0000000000000000000000000000000000000000"
181 );
fd87bb9Claude182 expect(out).toBe(false);
183 });
184});