Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

ai-tests.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.

ai-tests.test.tsBlame276 lines · 1 contributor
1e162a8Claude1/**
2 * Block D8 — AI-generated test suite tests.
3 *
4 * Pure-helper tests run directly; route tests tolerate the usual graceful
5 * degradation envelope (200 / 404 / 503) because these suites run without
6 * a live database.
7 */
8
9import { describe, it, expect } from "bun:test";
10import {
11 buildTestsPrompt,
12 contentTypeFor,
13 detectLanguage,
14 detectTestFramework,
15 generateTestStub,
16} from "../lib/ai-tests";
17
18// ---------------------------------------------------------------------------
19// detectLanguage
20// ---------------------------------------------------------------------------
21
22describe("lib/ai-tests — detectLanguage", () => {
23 it("maps .ts and .tsx to typescript", () => {
24 expect(detectLanguage("src/foo.ts")).toBe("typescript");
25 expect(detectLanguage("src/Foo.tsx")).toBe("typescript");
26 });
27
28 it("maps .js and .jsx to javascript", () => {
29 expect(detectLanguage("lib/foo.js")).toBe("javascript");
30 expect(detectLanguage("lib/bar.jsx")).toBe("javascript");
31 });
32
33 it("maps .py to python", () => {
34 expect(detectLanguage("pkg/mod.py")).toBe("python");
35 });
36
37 it("maps .go to go", () => {
38 expect(detectLanguage("cmd/main.go")).toBe("go");
39 });
40
41 it("maps .rs to rust", () => {
42 expect(detectLanguage("src/lib.rs")).toBe("rust");
43 });
44
45 it("maps unknown / extensionless to other", () => {
46 expect(detectLanguage("notes.txt")).toBe("other");
47 expect(detectLanguage("Makefile")).toBe("other");
48 expect(detectLanguage("README")).toBe("other");
49 });
50});
51
52// ---------------------------------------------------------------------------
53// detectTestFramework
54// ---------------------------------------------------------------------------
55
56describe("lib/ai-tests — detectTestFramework", () => {
57 it("returns bun:test when repo looks bun-ish with jest-style test files", () => {
58 const f = detectTestFramework("typescript", [
59 "package.json",
60 "bun.lockb",
61 "src/__tests__/foo.test.ts",
62 "src/__tests__/bar.test.ts",
63 ]);
64 expect(f).toBe("bun:test");
65 });
66
67 it("returns vitest when vitest.config.ts is present", () => {
68 const f = detectTestFramework("typescript", [
69 "package.json",
70 "vitest.config.ts",
71 "src/foo.ts",
72 ]);
73 expect(f).toBe("vitest");
74 });
75
76 it("returns jest when a jest.config.* file is present", () => {
77 const f = detectTestFramework("javascript", [
78 "package.json",
79 "jest.config.js",
80 ]);
81 expect(f).toBe("jest");
82 });
83
84 it("returns pytest when pytest.ini is present", () => {
85 const f = detectTestFramework("python", [
86 "pytest.ini",
87 "pkg/__init__.py",
88 "pkg/mod.py",
89 ]);
90 expect(f).toBe("pytest");
91 });
92
93 it("returns pytest for python regardless of signals (sensible default)", () => {
94 expect(detectTestFramework("python", [])).toBe("pytest");
95 });
96
97 it("returns go test for go language", () => {
98 expect(detectTestFramework("go", ["go.mod"])).toBe("go test");
99 });
100
101 it("falls back to bun:test when repoFiles is empty", () => {
102 expect(detectTestFramework("typescript", [])).toBe("bun:test");
103 expect(detectTestFramework("javascript", [])).toBe("bun:test");
104 expect(detectTestFramework("other", [])).toBe("bun:test");
105 });
106});
107
108// ---------------------------------------------------------------------------
109// buildTestsPrompt
110// ---------------------------------------------------------------------------
111
112describe("lib/ai-tests — buildTestsPrompt", () => {
113 it("embeds the source code and file path", () => {
114 const src = "export function add(a: number, b: number): number { return a + b; }";
115 const prompt = buildTestsPrompt({
116 path: "src/math.ts",
117 language: "typescript",
118 framework: "bun:test",
119 sourceCode: src,
120 });
121 expect(prompt).toContain("src/math.ts");
122 expect(prompt).toContain("bun:test");
123 expect(prompt).toContain(src);
124 });
125
126 it("explicitly instructs Claude to emit FAILING stubs", () => {
127 const prompt = buildTestsPrompt({
128 path: "foo.ts",
129 language: "typescript",
130 framework: "bun:test",
131 sourceCode: "export const x = 1;",
132 });
133 // Allow either casing — implementation uses *failing* with emphasis.
134 expect(prompt.toLowerCase()).toContain("failing");
135 });
136
137 it("includes optional apiHints when provided", () => {
138 const prompt = buildTestsPrompt({
139 path: "foo.py",
140 language: "python",
141 framework: "pytest",
142 sourceCode: "def add(a, b): return a + b",
143 apiHints: "add() returns the arithmetic sum",
144 });
145 expect(prompt).toContain("add() returns the arithmetic sum");
146 });
147
148 it("truncates excessively large source files", () => {
149 const huge = "a".repeat(50_000);
150 const prompt = buildTestsPrompt({
151 path: "big.ts",
152 language: "typescript",
153 framework: "bun:test",
154 sourceCode: huge,
155 });
156 expect(prompt.length).toBeLessThan(huge.length + 4_000);
157 expect(prompt).toContain("truncated");
158 });
159});
160
161// ---------------------------------------------------------------------------
162// generateTestStub
163// ---------------------------------------------------------------------------
164
165describe("lib/ai-tests — generateTestStub (AI unavailable)", () => {
166 const hadKey = !!process.env.ANTHROPIC_API_KEY;
167 const originalKey = process.env.ANTHROPIC_API_KEY;
168
169 // Ensure we exercise the AI-unavailable path deterministically.
170 if (hadKey) delete process.env.ANTHROPIC_API_KEY;
171
172 it("returns empty code and framework=fallback when no API key is set", async () => {
173 const result = await generateTestStub({
174 path: "src/lib/foo.ts",
175 language: "typescript",
176 framework: "bun:test",
177 sourceCode: "export const x = 1;",
178 });
179 expect(result.code).toBe("");
180 expect(result.framework).toBe("fallback");
181 expect(result.language).toBe("typescript");
182 });
183
184 it("still computes a sensible suggestedPath for bun:test", async () => {
185 const result = await generateTestStub({
186 path: "src/lib/foo.ts",
187 language: "typescript",
188 framework: "bun:test",
189 sourceCode: "export const x = 1;",
190 });
191 expect(result.suggestedPath).toContain("foo");
192 expect(result.suggestedPath).toContain(".test.");
193 });
194
195 it("picks `test_*.py` for pytest", async () => {
196 const result = await generateTestStub({
197 path: "pkg/widgets.py",
198 language: "python",
199 framework: "pytest",
200 sourceCode: "def f(): pass",
201 });
202 expect(result.suggestedPath.endsWith("test_widgets.py")).toBe(true);
203 });
204
205 it("picks `*_test.go` for go test", async () => {
206 const result = await generateTestStub({
207 path: "cmd/server.go",
208 language: "go",
209 framework: "go test",
210 sourceCode: "package main",
211 });
212 expect(result.suggestedPath.endsWith("server_test.go")).toBe(true);
213 });
214
215 // Restore the env we found on entry — other test files may depend on it.
216 if (hadKey && originalKey !== undefined) {
217 process.env.ANTHROPIC_API_KEY = originalKey;
218 }
219});
220
221// ---------------------------------------------------------------------------
222// contentTypeFor
223// ---------------------------------------------------------------------------
224
225describe("lib/ai-tests — contentTypeFor", () => {
226 it("returns a typescript MIME for typescript", () => {
227 expect(contentTypeFor("typescript")).toContain("typescript");
228 });
229 it("returns a python MIME for python", () => {
230 expect(contentTypeFor("python")).toContain("python");
231 });
232 it("returns a safe plain fallback for unknown", () => {
233 expect(contentTypeFor("other")).toContain("text/plain");
234 });
235});
236
237// ---------------------------------------------------------------------------
238// Route-level guard tests
239// ---------------------------------------------------------------------------
240
241describe("routes/ai-tests — guards", () => {
242 it("GET /:owner/:repo/ai/tests renders a form or 404s when repo doesn't exist", async () => {
243 const { default: aiTestsRoutes } = await import("../routes/ai-tests");
244 const res = await aiTestsRoutes.request("/alice/does-not-exist/ai/tests");
245 // 200 means the page rendered a form (repo exists, somehow), 404 means
246 // our handler matched but the repo row was absent, 503 means the DB
247 // proxy was down. Any of those is acceptable in CI.
248 expect([200, 404, 503]).toContain(res.status);
249 });
250
251 it("POST /:owner/:repo/ai/tests/generate without auth redirects to /login or 404s", async () => {
252 const { default: aiTestsRoutes } = await import("../routes/ai-tests");
253 const res = await aiTestsRoutes.request(
254 "/alice/does-not-exist/ai/tests/generate",
255 {
256 method: "POST",
257 redirect: "manual",
258 headers: { "content-type": "application/x-www-form-urlencoded" },
259 body: "path=src/foo.ts",
260 }
261 );
262 expect([302, 303, 404, 503]).toContain(res.status);
263 if (res.status === 302 || res.status === 303) {
264 const loc = res.headers.get("location") || "";
265 expect(loc).toContain("/login");
266 }
267 });
268
269 it("GET /:owner/:repo/ai/tests?format=raw without path returns 400 / 404 / 503", async () => {
270 const { default: aiTestsRoutes } = await import("../routes/ai-tests");
271 const res = await aiTestsRoutes.request(
272 "/alice/does-not-exist/ai/tests?format=raw"
273 );
274 expect([400, 404, 503]).toContain(res.status);
275 });
276});