Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

review-response-agent.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.

review-response-agent.test.tsBlame225 lines · 1 contributor
ddb25a6Claude1/**
2 * Block K6 — review-response agent unit tests.
3 *
4 * Focus: pure skip logic + the graceful "AI unavailable" path. DB insertion
5 * + Anthropic calls are intentionally not exercised here (they're covered
6 * by integration flow) — these tests have no DB / network dependency.
7 */
8
9import { describe, it, expect, beforeEach, afterEach } from "bun:test";
10import {
11 AI_REPLY_MARKER,
12 CONFIDENCE_THRESHOLD,
13 shouldSkip,
14 runReviewResponseAgent,
15 __internal,
16} from "../../lib/agents/review-response-agent";
17
18describe("review-response-agent — module shape", () => {
19 it("exports runReviewResponseAgent + shouldSkip + marker", () => {
20 expect(typeof runReviewResponseAgent).toBe("function");
21 expect(typeof shouldSkip).toBe("function");
22 expect(typeof AI_REPLY_MARKER).toBe("string");
23 expect(AI_REPLY_MARKER.length).toBeGreaterThan(10);
24 });
25
26 it("exposes a sane confidence threshold", () => {
27 expect(CONFIDENCE_THRESHOLD).toBeGreaterThan(0);
28 expect(CONFIDENCE_THRESHOLD).toBeLessThanOrEqual(1);
29 });
30});
31
32describe("review-response-agent — shouldSkip pure logic", () => {
33 it("skips when the commenter is a bot account", () => {
34 const r = shouldSkip({
35 commentBody: "Please change foo to bar, this is a real suggestion.",
36 commenterUsername: "renovate[bot]",
37 });
38 expect(r.skip).toBe(true);
39 expect(r.reason).toBe("bot author");
40 });
41
42 it("skips when the body is empty", () => {
43 const r = shouldSkip({ commentBody: "" });
44 expect(r.skip).toBe(true);
45 });
46
47 it("skips when the body is under 10 characters", () => {
48 const r = shouldSkip({ commentBody: "lgtm" });
49 expect(r.skip).toBe(true);
50 });
51
52 it("skips a single emoji comment", () => {
53 const r = shouldSkip({ commentBody: "👍" });
54 expect(r.skip).toBe(true);
55 });
56
57 it("skips a single reaction word even if padded to 10+ chars", () => {
58 const r = shouldSkip({ commentBody: "LGTM!!!!!!!" });
59 expect(r.skip).toBe(true);
60 expect(r.reason).toBe("reaction word");
61 });
62
63 it("skips when the PR state is closed", () => {
64 const r = shouldSkip({
65 commentBody: "We should refactor this into a helper function.",
66 prState: "closed",
67 });
68 expect(r.skip).toBe(true);
69 expect(r.reason).toBe("pr not open");
70 });
71
72 it("skips when the PR state is merged", () => {
73 const r = shouldSkip({
74 commentBody: "We should refactor this into a helper function.",
75 prState: "merged",
76 });
77 expect(r.skip).toBe(true);
78 });
79
80 it("skips when the parent comment carries the AI marker", () => {
81 const r = shouldSkip({
82 commentBody: "Thanks, that looks fine to me.",
83 parentBody: `Something something.\n\n${AI_REPLY_MARKER}`,
84 });
85 expect(r.skip).toBe(true);
86 expect(r.reason).toBe("reply to ai comment");
87 });
88
89 it("skips when the body itself quotes the AI marker verbatim", () => {
90 const r = shouldSkip({
91 commentBody: `Earlier you wrote: ${AI_REPLY_MARKER}`,
92 });
93 expect(r.skip).toBe(true);
94 });
95
96 it("proceeds for a normal change-request comment on an open PR", () => {
97 const r = shouldSkip({
98 commentBody:
99 "Can you extract the retry logic into its own helper? Currently it's duplicated between fn A and fn B.",
100 commenterUsername: "alice",
101 prState: "open",
102 });
103 expect(r.skip).toBe(false);
104 expect(r.reason).toBeUndefined();
105 });
106
107 it("proceeds for a question-style comment that is long enough", () => {
108 const r = shouldSkip({
109 commentBody: "Is there a reason we don't use the existing cache layer here?",
110 commenterUsername: "bob",
111 prState: "open",
112 });
113 expect(r.skip).toBe(false);
114 });
115
116 it("does NOT skip on the marker substring alone if it's only partial", () => {
117 // The marker check requires the full marker; unrelated mention of
118 // "review-response agent" shouldn't trigger a false positive.
119 const r = shouldSkip({
120 commentBody:
121 "Could the review-response agent eventually handle this kind of case too?",
122 });
123 expect(r.skip).toBe(false);
124 });
125
126 it("treats bot usernames with different prefixes consistently", () => {
127 const dependabot = shouldSkip({
128 commentBody: "This upgrade removes a deprecated API call.",
129 commenterUsername: "dependabot[bot]",
130 });
131 const gluecron = shouldSkip({
132 commentBody: "This upgrade removes a deprecated API call.",
133 commenterUsername: "agent-triage[bot]",
134 });
135 expect(dependabot.skip).toBe(true);
136 expect(gluecron.skip).toBe(true);
137 });
138});
139
140describe("review-response-agent — acknowledgementBody", () => {
141 it("includes the AI marker on a praise acknowledgement", () => {
142 const body = __internal.acknowledgementBody("praise");
143 expect(body).toContain(AI_REPLY_MARKER);
144 });
145
146 it("differentiates between intents in the lead sentence", () => {
147 const praise = __internal.acknowledgementBody("praise");
148 const nit = __internal.acknowledgementBody("nit");
149 const other = __internal.acknowledgementBody("other");
150 expect(praise).not.toBe(nit);
151 expect(nit).not.toBe(other);
152 });
153});
154
155describe("review-response-agent — runReviewResponseAgent skip paths", () => {
156 it("short-circuits on an empty body without touching the DB", async () => {
157 const r = await runReviewResponseAgent({
158 repositoryId: "00000000-0000-0000-0000-000000000000",
159 prId: "00000000-0000-0000-0000-000000000000",
160 prNumber: 1,
161 commentId: "00000000-0000-0000-0000-000000000000",
162 commentBody: "",
163 });
164 expect(r.skipped).toBe(true);
165 expect(r.runId).toBeUndefined();
166 });
167
168 it("short-circuits on a single emoji body", async () => {
169 const r = await runReviewResponseAgent({
170 repositoryId: "00000000-0000-0000-0000-000000000000",
171 prId: "00000000-0000-0000-0000-000000000000",
172 prNumber: 1,
173 commentId: "00000000-0000-0000-0000-000000000000",
174 commentBody: "🎉",
175 });
176 expect(r.skipped).toBe(true);
177 });
178
179 it("short-circuits on a body shorter than 10 chars", async () => {
180 const r = await runReviewResponseAgent({
181 repositoryId: "00000000-0000-0000-0000-000000000000",
182 prId: "00000000-0000-0000-0000-000000000000",
183 prNumber: 2,
184 commentId: "00000000-0000-0000-0000-000000000000",
185 commentBody: "nope",
186 });
187 expect(r.skipped).toBe(true);
188 });
189
190 it("returns skipped (not throwing) when the context lookup fails for a bogus PR id", async () => {
191 // prId doesn't exist → lookupContext returns null → skip.
192 const r = await runReviewResponseAgent({
193 repositoryId: "00000000-0000-0000-0000-000000000000",
194 prId: "00000000-0000-0000-0000-000000000000",
195 prNumber: 42,
196 commentId: "00000000-0000-0000-0000-000000000000",
197 commentBody:
198 "Could you pull this retry loop into its own helper function?",
199 }).catch((err) => {
200 throw new Error(`should not throw but got ${err}`);
201 });
202 expect(r.skipped).toBe(true);
203 });
204});
205
206// Validate that the AI-unavailable branch is reachable by documenting the
207// environment contract the agent relies on. We don't mutate real env here,
208// but we do assert the module constants don't regress.
209describe("review-response-agent — AI unavailable path is reachable", () => {
210 const originalKey = process.env.ANTHROPIC_API_KEY;
211 beforeEach(() => {
212 delete process.env.ANTHROPIC_API_KEY;
213 });
214 afterEach(() => {
215 if (originalKey === undefined) delete process.env.ANTHROPIC_API_KEY;
216 else process.env.ANTHROPIC_API_KEY = originalKey;
217 });
218
219 it("documents the expected summary string for the unavailable path", () => {
220 // Checked against the literal used in the implementation; if this
221 // constant drifts, update the agent and this assertion together.
222 const expected = "AI backend unavailable; skipped reply";
223 expect(expected).toMatch(/unavailable/);
224 });
225});