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

fix-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.

fix-agent.test.tsBlame269 lines · 1 contributor
055ebc4Claude1/**
2 * Block K5 — fix-agent tests.
3 *
4 * Same shape as heal-bot.test.ts:
5 * 1. Pure helpers (`renderFixAgentComment`, `buildFixAgentSummary`) —
6 * exercised without any I/O.
7 * 2. `runFixAgent` argument validation.
8 * 3. `runFixAgent` graceful-degradation when Gatetest + DB are unreachable.
9 */
10
11import {
12 afterEach,
13 beforeEach,
14 describe,
15 expect,
16 it,
17} from "bun:test";
18import {
19 renderFixAgentComment,
20 buildFixAgentSummary,
21 runFixAgent,
22 FIX_AGENT_BOT_USERNAME,
23 FIX_AGENT_SLUG,
24 FIX_AGENT_COST_CENTS,
25 FIX_AGENT_MAX_REPAIRS_IN_COMMENT,
26} from "../../lib/agents/fix-agent";
27
28const ENV_KEYS = ["GATETEST_API_KEY", "GATETEST_BASE_URL"] as const;
29
30let savedEnv: Record<string, string | undefined> = {};
31let savedFetch: typeof fetch;
32
33beforeEach(() => {
34 savedEnv = {};
35 for (const k of ENV_KEYS) savedEnv[k] = process.env[k];
36 for (const k of ENV_KEYS) delete process.env[k];
37 savedFetch = globalThis.fetch;
38});
39
40afterEach(() => {
41 for (const k of ENV_KEYS) {
42 const v = savedEnv[k];
43 if (v === undefined) delete process.env[k];
44 else process.env[k] = v;
45 }
46 globalThis.fetch = savedFetch;
47});
48
49// ---------------------------------------------------------------------------
50// Identity constants
51// ---------------------------------------------------------------------------
52
53describe("fix-agent — identity constants", () => {
54 it("uses the agent- prefixed slug", () => {
55 expect(FIX_AGENT_SLUG).toBe("agent-fix");
56 });
57 it("uses the [bot] suffixed username", () => {
58 expect(FIX_AGENT_BOT_USERNAME).toBe("agent-fix[bot]");
59 expect(FIX_AGENT_BOT_USERNAME.endsWith("[bot]")).toBe(true);
60 });
61 it("cost is flat 3¢", () => {
62 expect(FIX_AGENT_COST_CENTS).toBe(3);
63 });
64});
65
66// ---------------------------------------------------------------------------
67// renderFixAgentComment
68// ---------------------------------------------------------------------------
69
70describe("fix-agent — renderFixAgentComment", () => {
71 it("renders the passing case with all-clear wording", () => {
72 const body = renderFixAgentComment({
73 passed: true,
74 totalTests: 47,
75 failedBefore: 3,
76 failedAfter: 0,
77 repairs: [
78 { file: "src/foo.ts", before: "a", after: "b", reason: "null check" },
79 ],
80 unfixable: [],
81 });
82 expect(body).toContain("Fix Agent");
83 expect(body).toContain("47");
84 expect(body).toContain("all passing");
85 expect(body).toContain("src/foo.ts");
86 expect(body).toContain(FIX_AGENT_BOT_USERNAME);
87 });
88
89 it("renders the failing case with before/after counts", () => {
90 const body = renderFixAgentComment({
91 passed: false,
92 totalTests: 10,
93 failedBefore: 5,
94 failedAfter: 2,
95 repairs: [
96 { file: "a.ts", before: "", after: "", reason: "x" },
97 { file: "b.ts", before: "", after: "", reason: "y" },
98 { file: "c.ts", before: "", after: "", reason: "z" },
99 ],
100 unfixable: [{ file: "d.ts", reason: "unsupported language" }],
101 });
102 expect(body).toContain("**3** repairs");
103 expect(body).toContain("5 → 2");
104 expect(body).toContain("Unfixable");
105 expect(body).toContain("d.ts");
106 });
107
108 it("caps repairs list at FIX_AGENT_MAX_REPAIRS_IN_COMMENT and shows overflow count", () => {
109 const many = Array.from({ length: 30 }, (_, i) => ({
110 file: `file${i}.ts`,
111 before: "",
112 after: "",
113 reason: "r",
114 }));
115 const body = renderFixAgentComment({
116 passed: true,
117 totalTests: 30,
118 failedBefore: 30,
119 failedAfter: 0,
120 repairs: many,
121 unfixable: [],
122 });
123 expect(body).toContain(`file${FIX_AGENT_MAX_REPAIRS_IN_COMMENT - 1}.ts`);
124 expect(body).not.toContain(`file${FIX_AGENT_MAX_REPAIRS_IN_COMMENT}.ts`);
125 expect(body).toContain(
126 `…and ${30 - FIX_AGENT_MAX_REPAIRS_IN_COMMENT} more`
127 );
128 });
129
130 it("omits repairs section when there are no repairs but unfixable entries", () => {
131 const body = renderFixAgentComment({
132 passed: false,
133 totalTests: 1,
134 failedBefore: 1,
135 failedAfter: 1,
136 repairs: [],
137 unfixable: [{ file: "x.ts", reason: "too complex" }],
138 });
139 expect(body).not.toContain("### Repairs");
140 expect(body).toContain("### Unfixable");
141 expect(body).toContain("x.ts");
142 });
143
144 it("uses singular 'repair' for exactly one proposed", () => {
145 const body = renderFixAgentComment({
146 passed: false,
147 totalTests: 2,
148 failedBefore: 2,
149 failedAfter: 1,
150 repairs: [{ file: "f.ts", before: "", after: "", reason: "r" }],
151 unfixable: [],
152 });
153 expect(body).toContain("**1** repair ");
154 expect(body).not.toContain("**1** repairs");
155 });
156});
157
158// ---------------------------------------------------------------------------
159// buildFixAgentSummary
160// ---------------------------------------------------------------------------
161
162describe("fix-agent — buildFixAgentSummary", () => {
163 it("returns offline message when gatetest is offline", () => {
164 const s = buildFixAgentSummary({
165 offline: true,
166 passed: false,
167 failedBefore: 0,
168 failedAfter: 0,
169 repairs: 0,
170 });
171 expect(s).toBe("gatetest offline; skipped");
172 });
173
174 it("returns healthy message when nothing to fix", () => {
175 const s = buildFixAgentSummary({
176 offline: false,
177 passed: true,
178 failedBefore: 0,
179 failedAfter: 0,
180 repairs: 0,
181 });
182 expect(s).toContain("suite healthy");
183 });
184
185 it("reports failing-with-no-repairs case", () => {
186 const s = buildFixAgentSummary({
187 offline: false,
188 passed: false,
189 failedBefore: 5,
190 failedAfter: 5,
191 repairs: 0,
192 });
193 expect(s).toContain("5 failing");
194 expect(s).toContain("0 repairs");
195 });
196
197 it("reports full-repair success", () => {
198 const s = buildFixAgentSummary({
199 offline: false,
200 passed: true,
201 failedBefore: 3,
202 failedAfter: 0,
203 repairs: 3,
204 });
205 expect(s).toBe("repaired 3 (3 → 0)");
206 });
207
208 it("reports partial-repair case with plural repairs", () => {
209 const s = buildFixAgentSummary({
210 offline: false,
211 passed: false,
212 failedBefore: 5,
213 failedAfter: 2,
214 repairs: 3,
215 });
216 expect(s).toContain("3 repairs");
217 expect(s).toContain("5 → 2");
218 });
219
220 it("reports singular 'repair' for one", () => {
221 const s = buildFixAgentSummary({
222 offline: false,
223 passed: false,
224 failedBefore: 1,
225 failedAfter: 0,
226 repairs: 1,
227 });
228 expect(s).toContain("1 repair ");
229 expect(s).not.toContain("1 repairs");
230 });
231});
232
233// ---------------------------------------------------------------------------
234// runFixAgent — arg validation + graceful degradation.
235// ---------------------------------------------------------------------------
236
237describe("fix-agent — runFixAgent", () => {
238 it("rejects missing repositoryId without throwing", async () => {
239 const r = await runFixAgent({
240 repositoryId: "",
241 pullRequestId: "00000000-0000-0000-0000-000000000000",
242 });
243 expect(r.ok).toBe(false);
244 expect(r.runId).toBeNull();
245 expect(r.summary.toLowerCase()).toContain("invalid args");
246 });
247
248 it("rejects missing pullRequestId without throwing", async () => {
249 const r = await runFixAgent({
250 repositoryId: "00000000-0000-0000-0000-000000000000",
251 pullRequestId: "",
252 });
253 expect(r.ok).toBe(false);
254 expect(r.runId).toBeNull();
255 });
256
257 it("returns documented failure when DB cannot open a run", async () => {
258 globalThis.fetch = (() => {
259 throw new Error("fetch must not be called when run cannot be opened");
260 }) as unknown as typeof fetch;
261 const r = await runFixAgent({
262 repositoryId: "00000000-0000-0000-0000-000000000000",
263 pullRequestId: "00000000-0000-0000-0000-000000000000",
264 });
265 expect(r.ok).toBe(false);
266 expect(r.runId).toBeNull();
267 expect(r.summary.toLowerCase()).toMatch(/agent_runs|could not/);
268 });
269});