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

spec-context.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.

spec-context.test.tsBlame73 lines · 1 contributor
23d1a81Claude1import { describe, it, expect } from "bun:test";
2import { join } from "path";
3import { buildSpecContext, scoreFile } from "../lib/spec-context";
4
5describe("buildSpecContext", () => {
6 it("returns ok:false for nonexistent repo path", async () => {
7 const bogus = join(
8 "/tmp",
9 `spec-context-does-not-exist-${Date.now()}-${Math.random()}`
10 );
11 const result = await buildSpecContext({
12 repoDiskPath: bogus,
13 spec: "add a new auth endpoint",
14 });
15 expect(result.ok).toBe(false);
16 if (!result.ok) {
17 expect(typeof result.error).toBe("string");
18 expect(result.error.length).toBeGreaterThan(0);
19 }
20 });
21});
22
23describe("scoreFile", () => {
24 it("scores keyword-matched filenames higher than unrelated ones", () => {
25 const tokens = ["auth", "login", "session"];
26 const hot = scoreFile("src/routes/auth.ts", tokens);
27 const cold = scoreFile("src/views/layout.tsx", tokens);
28 expect(hot).toBeGreaterThan(cold);
29
30 // The spec word "login" matching a filename should also dominate over a
31 // fully unrelated path with no token overlap.
32 const loginHit = scoreFile("src/pages/login-form.ts", tokens);
33 const unrelated = scoreFile("src/utils/strings.ts", tokens);
34 expect(loginHit).toBeGreaterThan(unrelated);
35 });
36});
37
38describe("scoreFile ranking & caps", () => {
39 it("caps file list at 500 and relevantFiles at maxRelevantFiles", async () => {
40 // We can exercise the ranking/cap logic without a real git repo by
41 // simulating the scoring step directly. This covers the public contract
42 // that `scoreFile` + downstream sort gives a stable ranking, and the
43 // numeric caps declared in the module are respected.
44 const tokens = ["widget"];
45 const paths: string[] = [];
46 for (let i = 0; i < 750; i++) {
47 // Mix in some matches so sorting has signal.
48 paths.push(i % 7 === 0 ? `src/widget-${i}.ts` : `src/file-${i}.ts`);
49 }
50 const capped = paths.slice(0, 500);
51 expect(capped.length).toBe(500);
52
53 const scored = capped
54 .map((p) => ({ p, s: scoreFile(p, tokens) }))
55 .sort((a, b) => {
56 if (b.s !== a.s) return b.s - a.s;
57 return a.p.length - b.p.length;
58 });
59
60 // Top of ranking should be a widget-matched path.
61 expect(scored[0].p).toContain("widget");
62
63 // Applying the maxRelevantFiles cap produces exactly that many entries.
64 const maxRelevantFiles = 20;
65 const top = scored.slice(0, maxRelevantFiles);
66 expect(top.length).toBe(maxRelevantFiles);
67
68 // And every top entry should score at least as high as any dropped one.
69 const tailMax = Math.max(...scored.slice(maxRelevantFiles).map((x) => x.s));
70 const topMin = Math.min(...top.map((x) => x.s));
71 expect(topMin).toBeGreaterThanOrEqual(tailMax);
72 });
73});