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

ai-changelog.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-changelog.test.tsBlame138 lines · 2 contributors
3cbe3d6Claude1/**
2 * Block D7 — AI changelog route tests.
3 *
4 * The route depends on a real git repo on disk, so these tests drive the
5 * route via the exported Hono app directly. A temporary bare repository is
6 * spun up per test run under a unique GIT_REPOS_PATH so we don't collide
7 * with other tests or the developer's local `./repos` tree.
8 *
9 * Tests here are deliberately scoped to the guarantees the Block D7 spec
10 * calls out:
11 * - Module imports cleanly.
12 * - GET without query params renders the picker form (HTML).
13 * - GET with nonsense from/to strings renders an error banner, NOT a 500.
14 * - GET for a non-existent repo returns 404.
15 */
16
17import { describe, it, expect, beforeAll, afterAll } from "bun:test";
18import { mkdtemp, rm, mkdir, writeFile } from "fs/promises";
19import { tmpdir } from "os";
20import { join } from "path";
21
22// Pin GIT_REPOS_PATH BEFORE importing the route so `config.gitReposPath`
23// (which is a lazy getter) reads our scratch dir on first access.
24const SCRATCH = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-"));
25process.env.GIT_REPOS_PATH = SCRATCH;
26
27// eslint-disable-next-line import/first
28const { default: aiChangelog } = await import("../routes/ai-changelog");
29
30const OWNER = "alice";
31const REPO = "demo";
32
33async function run(cmd: string[], cwd: string) {
34 const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
779c681ccantynz-alt35 const __code = await proc.exited;
36 // Fail loudly. These fixtures sit under the project dir, so a silently
37 // failed clone leaves cwd inside the REAL repo and the following git
38 // config/add/commit then mutate it. See week3.test.ts.
39 if (__code !== 0) throw new Error(`git fixture command failed (exit ${__code})`);
3cbe3d6Claude40}
41
42async function seedRepo(): Promise<void> {
43 // Bare repo lives where getRepoPath expects: <root>/<owner>/<repo>.git
44 const bareDir = join(SCRATCH, OWNER, `${REPO}.git`);
45 await mkdir(bareDir, { recursive: true });
46 await run(["git", "init", "--bare", bareDir], SCRATCH);
47 await run(
48 ["git", "symbolic-ref", "HEAD", "refs/heads/main"],
49 bareDir
50 );
51
52 // Push one real commit from a scratch working tree so branches/refs exist.
53 const workDir = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-work-"));
54 await run(["git", "init", workDir], SCRATCH);
55 await run(
56 ["git", "-C", workDir, "config", "user.email", "test@example.com"],
57 SCRATCH
58 );
59 await run(
60 ["git", "-C", workDir, "config", "user.name", "Test"],
61 SCRATCH
62 );
63 await writeFile(join(workDir, "README.md"), "# hello\n");
64 await run(["git", "-C", workDir, "add", "README.md"], SCRATCH);
65 await run(
66 ["git", "-C", workDir, "commit", "-m", "initial commit"],
67 SCRATCH
68 );
69 // Ensure the local branch is called main regardless of git defaults.
70 await run(
71 ["git", "-C", workDir, "branch", "-M", "main"],
72 SCRATCH
73 );
74 await run(
75 ["git", "-C", workDir, "remote", "add", "origin", bareDir],
76 SCRATCH
77 );
78 await run(
79 ["git", "-C", workDir, "push", "origin", "main"],
80 SCRATCH
81 );
82
83 await rm(workDir, { recursive: true, force: true });
84}
85
86describe("routes/ai-changelog — module", () => {
87 it("imports cleanly and exports a Hono app", () => {
88 expect(aiChangelog).toBeDefined();
89 expect(typeof aiChangelog.request).toBe("function");
90 });
91});
92
93describe("routes/ai-changelog — repo present", () => {
94 beforeAll(async () => {
95 await seedRepo();
96 });
97
98 afterAll(async () => {
99 await rm(SCRATCH, { recursive: true, force: true }).catch(() => {});
100 });
101
102 it("GET without query params renders the picker form", async () => {
103 const res = await aiChangelog.request(
104 `/${OWNER}/${REPO}/ai/changelog`
105 );
106 expect(res.status).toBe(200);
107 const body = await res.text();
108 expect(body).toContain("AI Changelog");
109 // The form exposes from/to inputs and a submit button.
110 expect(body).toContain('name="from"');
111 expect(body).toContain('name="to"');
112 expect(body.toLowerCase()).toContain("generate");
113 });
114
115 it("GET with nonsense from/to renders an error banner, not a 500", async () => {
116 const res = await aiChangelog.request(
117 `/${OWNER}/${REPO}/ai/changelog?from=not-a-real-ref-xyz&to=also-not-real-abc`
118 );
119 // Must NOT be a 500 — the route should handle unresolvable refs gracefully.
120 expect(res.status).toBe(200);
121 const body = await res.text();
122 // Error banner class used by the Layout for form errors.
123 expect(body).toContain("auth-error");
124 // And mentions the offending ref(s).
125 expect(body).toMatch(/Could not resolve/i);
126 });
127});
128
129describe("routes/ai-changelog — missing repo", () => {
130 it("returns 404 for a non-existent repo", async () => {
131 const res = await aiChangelog.request(
132 "/nobody-xyz/nothing-xyz/ai/changelog"
133 );
134 expect(res.status).toBe(404);
135 const body = await res.text();
136 expect(body.toLowerCase()).toContain("repository not found");
137 });
138});