Blame · Line-by-line history
gatetest-client.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.
| a6d8fd5 | 1 | /** |
| 2 | * Block K — Gatetest client tests. | |
| 3 | * | |
| 4 | * Exercises the env-driven configuration toggle, the auth header builder, | |
| 5 | * and the offline short-circuits. When a key is present, we mock | |
| 6 | * globalThis.fetch with a 500 to confirm graceful degradation to the | |
| 7 | * offline result shape. | |
| 8 | */ | |
| 9 | ||
| 10 | import { describe, it, expect, beforeEach, afterEach } from "bun:test"; | |
| 11 | import { | |
| 12 | buildAuthHeaders, | |
| 13 | healSuite, | |
| 14 | isConfigured, | |
| 15 | runAndRepair, | |
| 16 | stackTraceToTest, | |
| 17 | } from "../lib/gatetest-client"; | |
| 18 | ||
| 19 | const ENV_KEYS = ["GATETEST_API_KEY", "GATETEST_BASE_URL"] as const; | |
| 20 | ||
| 21 | let savedEnv: Record<string, string | undefined> = {}; | |
| 22 | let savedFetch: typeof fetch; | |
| 23 | ||
| 24 | beforeEach(() => { | |
| 25 | savedEnv = {}; | |
| 26 | for (const k of ENV_KEYS) savedEnv[k] = process.env[k]; | |
| 27 | // Start every test with a clean slate. | |
| 28 | for (const k of ENV_KEYS) delete process.env[k]; | |
| 29 | savedFetch = globalThis.fetch; | |
| 30 | }); | |
| 31 | ||
| 32 | afterEach(() => { | |
| 33 | for (const k of ENV_KEYS) { | |
| 34 | const v = savedEnv[k]; | |
| 35 | if (v === undefined) delete process.env[k]; | |
| 36 | else process.env[k] = v; | |
| 37 | } | |
| 38 | globalThis.fetch = savedFetch; | |
| 39 | }); | |
| 40 | ||
| 41 | // --------------------------------------------------------------------------- | |
| 42 | // isConfigured | |
| 43 | // --------------------------------------------------------------------------- | |
| 44 | ||
| 45 | describe("gatetest-client — isConfigured", () => { | |
| 46 | it("returns false when no API key is set", () => { | |
| 47 | expect(isConfigured()).toBe(false); | |
| 48 | }); | |
| 49 | ||
| 50 | it("returns true once GATETEST_API_KEY is set", () => { | |
| 51 | process.env.GATETEST_API_KEY = "sk-test-abc"; | |
| 52 | expect(isConfigured()).toBe(true); | |
| 53 | }); | |
| 54 | ||
| 55 | it("flips back to false when the key is removed mid-process", () => { | |
| 56 | process.env.GATETEST_API_KEY = "sk-test-abc"; | |
| 57 | expect(isConfigured()).toBe(true); | |
| 58 | delete process.env.GATETEST_API_KEY; | |
| 59 | expect(isConfigured()).toBe(false); | |
| 60 | }); | |
| 61 | }); | |
| 62 | ||
| 63 | // --------------------------------------------------------------------------- | |
| 64 | // buildAuthHeaders | |
| 65 | // --------------------------------------------------------------------------- | |
| 66 | ||
| 67 | describe("gatetest-client — buildAuthHeaders", () => { | |
| 68 | it("returns only Content-Type when no key is present", () => { | |
| 69 | const h = buildAuthHeaders(); | |
| 70 | expect(h["Content-Type"]).toBe("application/json"); | |
| 71 | expect(h["Authorization"]).toBeUndefined(); | |
| 72 | }); | |
| 73 | ||
| 74 | it("includes a Bearer token when the key is present", () => { | |
| 75 | process.env.GATETEST_API_KEY = "sk-live-xyz"; | |
| 76 | const h = buildAuthHeaders(); | |
| 77 | expect(h["Content-Type"]).toBe("application/json"); | |
| 78 | expect(h["Authorization"]).toBe("Bearer sk-live-xyz"); | |
| 79 | }); | |
| 80 | }); | |
| 81 | ||
| 82 | // --------------------------------------------------------------------------- | |
| 83 | // runAndRepair | |
| 84 | // --------------------------------------------------------------------------- | |
| 85 | ||
| 86 | describe("gatetest-client — runAndRepair", () => { | |
| 87 | it("returns offline result with zeros when no API key is set", async () => { | |
| 88 | // No fetch mock — the method MUST short-circuit. | |
| 89 | globalThis.fetch = (() => { | |
| 90 | throw new Error("fetch must not be called in offline mode"); | |
| 91 | }) as unknown as typeof fetch; | |
| 92 | const result = await runAndRepair({ repo: "o/r", ref: "main" }); | |
| 93 | expect(result.offline).toBe(true); | |
| 94 | expect(result.passed).toBe(false); | |
| 95 | expect(result.totalTests).toBe(0); | |
| 96 | expect(result.failedBefore).toBe(0); | |
| 97 | expect(result.failedAfter).toBe(0); | |
| 98 | expect(result.repairs).toEqual([]); | |
| 99 | expect(result.unfixable).toEqual([]); | |
| 100 | expect(result.durationMs).toBe(0); | |
| 101 | }); | |
| 102 | ||
| 103 | it("falls back to offline on a 500 response", async () => { | |
| 104 | process.env.GATETEST_API_KEY = "sk-test"; | |
| 105 | globalThis.fetch = (async () => | |
| 106 | new Response("boom", { status: 500 })) as unknown as typeof fetch; | |
| 107 | const result = await runAndRepair({ repo: "o/r", ref: "main" }); | |
| 108 | expect(result.offline).toBe(true); | |
| 109 | expect(result.passed).toBe(false); | |
| 110 | expect(result.totalTests).toBe(0); | |
| 111 | }); | |
| 112 | ||
| 113 | it("parses a healthy 200 response into a structured result", async () => { | |
| 114 | process.env.GATETEST_API_KEY = "sk-test"; | |
| 115 | const payload = { | |
| 116 | passed: true, | |
| 117 | totalTests: 42, | |
| 118 | failedBefore: 3, | |
| 119 | failedAfter: 0, | |
| 120 | repairs: [ | |
| 121 | { file: "a.ts", before: "x", after: "y", reason: "flake" }, | |
| 122 | ], | |
| 123 | unfixable: [], | |
| 124 | durationMs: 12345, | |
| 125 | }; | |
| 126 | globalThis.fetch = (async () => | |
| 127 | new Response(JSON.stringify(payload), { | |
| 128 | status: 200, | |
| 129 | headers: { "Content-Type": "application/json" }, | |
| 130 | })) as unknown as typeof fetch; | |
| 131 | const result = await runAndRepair({ | |
| 132 | repo: "o/r", | |
| 133 | ref: "main", | |
| 134 | targetGlob: "src/**", | |
| 135 | }); | |
| 136 | expect(result.offline).toBe(false); | |
| 137 | expect(result.passed).toBe(true); | |
| 138 | expect(result.totalTests).toBe(42); | |
| 139 | expect(result.repairs).toHaveLength(1); | |
| 140 | }); | |
| 141 | }); | |
| 142 | ||
| 143 | // --------------------------------------------------------------------------- | |
| 144 | // stackTraceToTest | |
| 145 | // --------------------------------------------------------------------------- | |
| 146 | ||
| 147 | describe("gatetest-client — stackTraceToTest", () => { | |
| 148 | it("returns a deterministic offline stub when no key is set", async () => { | |
| 149 | const result = await stackTraceToTest({ | |
| 150 | repo: "o/r", | |
| 151 | stackTrace: "TypeError: cannot read 'x' of undefined\n at foo", | |
| 152 | language: "typescript", | |
| 153 | }); | |
| 154 | expect(result.offline).toBe(true); | |
| 155 | expect(result.framework).toBe("fallback"); | |
| 156 | expect(result.testCode).toContain("TODO"); | |
| 157 | expect(result.suggestedPath.endsWith(".test.ts")).toBe(true); | |
| 158 | }); | |
| 159 | ||
| 160 | it("picks a pytest path for python offline stubs", async () => { | |
| 161 | const result = await stackTraceToTest({ | |
| 162 | repo: "o/r", | |
| 163 | stackTrace: "AttributeError: foo", | |
| 164 | language: "python", | |
| 165 | }); | |
| 166 | expect(result.offline).toBe(true); | |
| 167 | expect(result.suggestedPath.endsWith(".py")).toBe(true); | |
| 168 | expect(result.testCode).toContain("def test_"); | |
| 169 | }); | |
| 170 | ||
| 171 | it("falls back to offline on a 500 response", async () => { | |
| 172 | process.env.GATETEST_API_KEY = "sk-test"; | |
| 173 | globalThis.fetch = (async () => | |
| 174 | new Response("down", { status: 500 })) as unknown as typeof fetch; | |
| 175 | const result = await stackTraceToTest({ | |
| 176 | repo: "o/r", | |
| 177 | stackTrace: "boom", | |
| 178 | }); | |
| 179 | expect(result.offline).toBe(true); | |
| 180 | expect(result.framework).toBe("fallback"); | |
| 181 | }); | |
| 182 | }); | |
| 183 | ||
| 184 | // --------------------------------------------------------------------------- | |
| 185 | // healSuite | |
| 186 | // --------------------------------------------------------------------------- | |
| 187 | ||
| 188 | describe("gatetest-client — healSuite", () => { | |
| 189 | it("returns offline zeros when no key is set", async () => { | |
| 190 | const result = await healSuite({ repo: "o/r" }); | |
| 191 | expect(result.offline).toBe(true); | |
| 192 | expect(result.flakyFound).toBe(0); | |
| 193 | expect(result.deadFound).toBe(0); | |
| 194 | expect(result.coverageGapsFound).toBe(0); | |
| 195 | expect(result.prDraftBranch).toBeNull(); | |
| 196 | }); | |
| 197 | ||
| 198 | it("falls back to offline on a 500 response", async () => { | |
| 199 | process.env.GATETEST_API_KEY = "sk-test"; | |
| 200 | globalThis.fetch = (async () => | |
| 201 | new Response("fail", { status: 500 })) as unknown as typeof fetch; | |
| 202 | const result = await healSuite({ repo: "o/r" }); | |
| 203 | expect(result.offline).toBe(true); | |
| 204 | expect(result.flakyFound).toBe(0); | |
| 205 | }); | |
| 206 | ||
| 207 | it("falls back to offline when fetch itself throws (network down)", async () => { | |
| 208 | process.env.GATETEST_API_KEY = "sk-test"; | |
| 209 | globalThis.fetch = (async () => { | |
| 210 | throw new Error("ECONNREFUSED"); | |
| 211 | }) as unknown as typeof fetch; | |
| 212 | const result = await healSuite({ repo: "o/r" }); | |
| 213 | expect(result.offline).toBe(true); | |
| 214 | expect(result.prDraftBranch).toBeNull(); | |
| 215 | }); | |
| 216 | ||
| 217 | it("parses a healthy 200 response", async () => { | |
| 218 | process.env.GATETEST_API_KEY = "sk-test"; | |
| 219 | globalThis.fetch = (async () => | |
| 220 | new Response( | |
| 221 | JSON.stringify({ | |
| 222 | flakyFound: 2, | |
| 223 | deadFound: 1, | |
| 224 | coverageGapsFound: 4, | |
| 225 | prDraftBranch: "gatetest/heal-42", | |
| 226 | }), | |
| 227 | { status: 200, headers: { "Content-Type": "application/json" } } | |
| 228 | )) as unknown as typeof fetch; | |
| 229 | const result = await healSuite({ repo: "o/r" }); | |
| 230 | expect(result.offline).toBe(false); | |
| 231 | expect(result.flakyFound).toBe(2); | |
| 232 | expect(result.prDraftBranch).toBe("gatetest/heal-42"); | |
| 233 | }); | |
| 234 | }); |