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.
| 055ebc4 | 1 | /** |
| 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 | ||
| 11 | import { | |
| 12 | afterEach, | |
| 13 | beforeEach, | |
| 14 | describe, | |
| 15 | expect, | |
| 16 | it, | |
| 17 | } from "bun:test"; | |
| 18 | import { | |
| 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 | ||
| 28 | const ENV_KEYS = ["GATETEST_API_KEY", "GATETEST_BASE_URL"] as const; | |
| 29 | ||
| 30 | let savedEnv: Record<string, string | undefined> = {}; | |
| 31 | let savedFetch: typeof fetch; | |
| 32 | ||
| 33 | beforeEach(() => { | |
| 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 | ||
| 40 | afterEach(() => { | |
| 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 | ||
| 53 | describe("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 | ||
| 70 | describe("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 | ||
| 162 | describe("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 | ||
| 237 | describe("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 | }); |