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

gatetest-client.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.

gatetest-client.tsBlame288 lines · 1 contributor
a6d8fd5Claude1/**
2 * Block K — Gatetest HTTP client.
3 *
4 * Typed tool primitives that K-agents call to drive Gatetest (external
5 * testing / test-repair platform). Each primitive hits the documented
6 * endpoint when `GATETEST_API_KEY` is set and falls back to a deterministic
7 * offline mode otherwise. No method throws — callers get a well-formed
8 * result with `offline: true` on any failure.
9 *
10 * Env vars (read lazily via getters so tests can flip them per-case):
11 * GATETEST_API_KEY — bearer token for the Gatetest API (required)
12 * GATETEST_BASE_URL — override base URL (default `https://gatetest.ai`)
13 *
14 * Endpoint shapes assumed (documented for the Gatetest team):
15 * POST {base}/api/v2/run-and-repair
16 * body: { repo, ref, targetGlob? }
17 * 200 -> {
18 * passed: boolean,
19 * totalTests: number,
20 * failedBefore: number,
21 * failedAfter: number,
22 * repairs: [{ file, before, after, reason }],
23 * unfixable: [{ file, reason }],
24 * durationMs: number
25 * }
26 * POST {base}/api/v2/stack-to-test
27 * body: { repo, stackTrace, language? }
28 * 200 -> { testCode, framework, suggestedPath }
29 * POST {base}/api/v2/heal-suite
30 * body: { repo }
31 * 200 -> {
32 * flakyFound: number,
33 * deadFound: number,
34 * coverageGapsFound: number,
35 * prDraftBranch: string | null
36 * }
37 */
38
39// ---------------------------------------------------------------------------
40// Env getters — read process.env at access time so tests can mutate freely.
41// ---------------------------------------------------------------------------
42
43export const gatetestEnv = {
44 get apiKey(): string {
45 return process.env.GATETEST_API_KEY || "";
46 },
47 get baseUrl(): string {
48 return (process.env.GATETEST_BASE_URL || "https://gatetest.ai").replace(
49 /\/+$/,
50 ""
51 );
52 },
53};
54
55export function isConfigured(): boolean {
56 return !!gatetestEnv.apiKey;
57}
58
59export function buildAuthHeaders(): Record<string, string> {
60 const headers: Record<string, string> = {
61 "Content-Type": "application/json",
62 };
63 const key = gatetestEnv.apiKey;
64 if (key) headers["Authorization"] = `Bearer ${key}`;
65 return headers;
66}
67
68// ---------------------------------------------------------------------------
69// Types
70// ---------------------------------------------------------------------------
71
72export type GatetestRepair = {
73 file: string;
74 before: string;
75 after: string;
76 reason: string;
77};
78
79export type GatetestUnfixable = {
80 file: string;
81 reason: string;
82};
83
84export type RunAndRepairResult = {
85 passed: boolean;
86 totalTests: number;
87 failedBefore: number;
88 failedAfter: number;
89 repairs: GatetestRepair[];
90 unfixable: GatetestUnfixable[];
91 durationMs: number;
92 offline: boolean;
93};
94
95export type StackTraceToTestResult = {
96 testCode: string;
97 framework: string;
98 suggestedPath: string;
99 offline: boolean;
100};
101
102export type HealSuiteResult = {
103 flakyFound: number;
104 deadFound: number;
105 coverageGapsFound: number;
106 prDraftBranch: string | null;
107 offline: boolean;
108};
109
110// ---------------------------------------------------------------------------
111// Offline defaults
112// ---------------------------------------------------------------------------
113
114function offlineRunAndRepair(): RunAndRepairResult {
115 return {
116 passed: false,
117 totalTests: 0,
118 failedBefore: 0,
119 failedAfter: 0,
120 repairs: [],
121 unfixable: [],
122 durationMs: 0,
123 offline: true,
124 };
125}
126
127function offlineHealSuite(): HealSuiteResult {
128 return {
129 flakyFound: 0,
130 deadFound: 0,
131 coverageGapsFound: 0,
132 prDraftBranch: null,
133 offline: true,
134 };
135}
136
137function offlineStackToTest(
138 stackTrace: string,
139 language?: string
140): StackTraceToTestResult {
141 // Deterministic stub test. Intentionally decoupled from ai-tests.ts —
142 // we're an offline fallback, not an AI-driven generator.
143 const firstLine = (stackTrace || "").split("\n")[0]?.trim() || "error";
144 const escaped = firstLine.replace(/[`\\]/g, "").slice(0, 200);
145 const lang = (language || "typescript").toLowerCase();
146 let suggestedPath = "tests/reproduce.test.ts";
147 let testCode = "";
148 if (lang === "python") {
149 suggestedPath = "tests/test_reproduce.py";
150 testCode =
151 `# TODO: Gatetest is offline — replace this stub with a real reproducer.\n` +
152 `# Seed stack-trace: ${escaped}\n` +
153 `def test_reproduce():\n` +
154 ` assert False, "offline stub: paste stack trace + repro here"\n`;
155 } else if (lang === "go") {
156 suggestedPath = "reproduce_test.go";
157 testCode =
158 `// TODO: Gatetest is offline — replace this stub with a real reproducer.\n` +
159 `// Seed stack-trace: ${escaped}\n` +
160 `package main\n\nimport "testing"\n\n` +
161 `func TestReproduce(t *testing.T) {\n` +
162 `\tt.Fatal("offline stub: paste stack trace + repro here")\n` +
163 `}\n`;
164 } else {
165 testCode =
166 `// TODO: Gatetest is offline — replace this stub with a real reproducer.\n` +
167 `// Seed stack-trace: ${escaped}\n` +
168 `import { test, expect } from "bun:test";\n\n` +
169 `test("reproduce", () => {\n` +
170 ` expect.unreachable("offline stub: paste stack trace + repro here");\n` +
171 `});\n`;
172 }
173 return {
174 testCode,
175 framework: "fallback",
176 suggestedPath,
177 offline: true,
178 };
179}
180
181// ---------------------------------------------------------------------------
182// Shared fetch-with-timeout helper. Never throws — returns null on any
183// failure so callers can flip to the offline branch.
184// ---------------------------------------------------------------------------
185
186async function postJson(
187 url: string,
188 body: unknown,
189 timeoutMs: number
190): Promise<unknown | null> {
191 const controller = new AbortController();
192 const timer = setTimeout(() => controller.abort(), timeoutMs);
193 try {
194 const res = await fetch(url, {
195 method: "POST",
196 headers: buildAuthHeaders(),
197 body: JSON.stringify(body ?? {}),
198 signal: controller.signal,
199 });
200 if (!res.ok) return null;
201 return await res.json().catch(() => null);
202 } catch {
203 return null;
204 } finally {
205 clearTimeout(timer);
206 }
207}
208
209// ---------------------------------------------------------------------------
210// Public primitives
211// ---------------------------------------------------------------------------
212
213export async function runAndRepair(params: {
214 repo: string;
215 ref: string;
216 targetGlob?: string;
217}): Promise<RunAndRepairResult> {
218 if (!isConfigured()) return offlineRunAndRepair();
219 const url = `${gatetestEnv.baseUrl}/api/v2/run-and-repair`;
220 const data = (await postJson(url, params, 5 * 60 * 1000)) as
221 | Partial<RunAndRepairResult>
222 | null;
223 if (!data || typeof data !== "object") return offlineRunAndRepair();
224 return {
225 passed: !!data.passed,
226 totalTests: Number(data.totalTests || 0),
227 failedBefore: Number(data.failedBefore || 0),
228 failedAfter: Number(data.failedAfter || 0),
229 repairs: Array.isArray(data.repairs) ? (data.repairs as GatetestRepair[]) : [],
230 unfixable: Array.isArray(data.unfixable)
231 ? (data.unfixable as GatetestUnfixable[])
232 : [],
233 durationMs: Number(data.durationMs || 0),
234 offline: false,
235 };
236}
237
238export async function stackTraceToTest(params: {
239 repo: string;
240 stackTrace: string;
241 language?: string;
242}): Promise<StackTraceToTestResult> {
243 if (!isConfigured()) {
244 return offlineStackToTest(params.stackTrace, params.language);
245 }
246 const url = `${gatetestEnv.baseUrl}/api/v2/stack-to-test`;
247 const data = (await postJson(url, params, 60 * 1000)) as
248 | Partial<StackTraceToTestResult>
249 | null;
250 if (!data || typeof data !== "object" || typeof data.testCode !== "string") {
251 return offlineStackToTest(params.stackTrace, params.language);
252 }
253 return {
254 testCode: data.testCode,
255 framework: typeof data.framework === "string" ? data.framework : "unknown",
256 suggestedPath:
257 typeof data.suggestedPath === "string"
258 ? data.suggestedPath
259 : "tests/reproduce.test.ts",
260 offline: false,
261 };
262}
263
264export async function healSuite(params: {
265 repo: string;
266}): Promise<HealSuiteResult> {
267 if (!isConfigured()) return offlineHealSuite();
268 const url = `${gatetestEnv.baseUrl}/api/v2/heal-suite`;
269 const data = (await postJson(url, params, 10 * 60 * 1000)) as
270 | Partial<HealSuiteResult>
271 | null;
272 if (!data || typeof data !== "object") return offlineHealSuite();
273 return {
274 flakyFound: Number(data.flakyFound || 0),
275 deadFound: Number(data.deadFound || 0),
276 coverageGapsFound: Number(data.coverageGapsFound || 0),
277 prDraftBranch:
278 typeof data.prDraftBranch === "string" ? data.prDraftBranch : null,
279 offline: false,
280 };
281}
282
283export const __internal = {
284 offlineRunAndRepair,
285 offlineHealSuite,
286 offlineStackToTest,
287 postJson,
288};