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-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.tsBlame134 lines · 1 contributor
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" });
35 await proc.exited;
36}
37
38async function seedRepo(): Promise<void> {
39 // Bare repo lives where getRepoPath expects: <root>/<owner>/<repo>.git
40 const bareDir = join(SCRATCH, OWNER, `${REPO}.git`);
41 await mkdir(bareDir, { recursive: true });
42 await run(["git", "init", "--bare", bareDir], SCRATCH);
43 await run(
44 ["git", "symbolic-ref", "HEAD", "refs/heads/main"],
45 bareDir
46 );
47
48 // Push one real commit from a scratch working tree so branches/refs exist.
49 const workDir = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-work-"));
50 await run(["git", "init", workDir], SCRATCH);
51 await run(
52 ["git", "-C", workDir, "config", "user.email", "test@example.com"],
53 SCRATCH
54 );
55 await run(
56 ["git", "-C", workDir, "config", "user.name", "Test"],
57 SCRATCH
58 );
59 await writeFile(join(workDir, "README.md"), "# hello\n");
60 await run(["git", "-C", workDir, "add", "README.md"], SCRATCH);
61 await run(
62 ["git", "-C", workDir, "commit", "-m", "initial commit"],
63 SCRATCH
64 );
65 // Ensure the local branch is called main regardless of git defaults.
66 await run(
67 ["git", "-C", workDir, "branch", "-M", "main"],
68 SCRATCH
69 );
70 await run(
71 ["git", "-C", workDir, "remote", "add", "origin", bareDir],
72 SCRATCH
73 );
74 await run(
75 ["git", "-C", workDir, "push", "origin", "main"],
76 SCRATCH
77 );
78
79 await rm(workDir, { recursive: true, force: true });
80}
81
82describe("routes/ai-changelog — module", () => {
83 it("imports cleanly and exports a Hono app", () => {
84 expect(aiChangelog).toBeDefined();
85 expect(typeof aiChangelog.request).toBe("function");
86 });
87});
88
89describe("routes/ai-changelog — repo present", () => {
90 beforeAll(async () => {
91 await seedRepo();
92 });
93
94 afterAll(async () => {
95 await rm(SCRATCH, { recursive: true, force: true }).catch(() => {});
96 });
97
98 it("GET without query params renders the picker form", async () => {
99 const res = await aiChangelog.request(
100 `/${OWNER}/${REPO}/ai/changelog`
101 );
102 expect(res.status).toBe(200);
103 const body = await res.text();
104 expect(body).toContain("AI Changelog");
105 // The form exposes from/to inputs and a submit button.
106 expect(body).toContain('name="from"');
107 expect(body).toContain('name="to"');
108 expect(body.toLowerCase()).toContain("generate");
109 });
110
111 it("GET with nonsense from/to renders an error banner, not a 500", async () => {
112 const res = await aiChangelog.request(
113 `/${OWNER}/${REPO}/ai/changelog?from=not-a-real-ref-xyz&to=also-not-real-abc`
114 );
115 // Must NOT be a 500 — the route should handle unresolvable refs gracefully.
116 expect(res.status).toBe(200);
117 const body = await res.text();
118 // Error banner class used by the Layout for form errors.
119 expect(body).toContain("auth-error");
120 // And mentions the offending ref(s).
121 expect(body).toMatch(/Could not resolve/i);
122 });
123});
124
125describe("routes/ai-changelog — missing repo", () => {
126 it("returns 404 for a non-existent repo", async () => {
127 const res = await aiChangelog.request(
128 "/nobody-xyz/nothing-xyz/ai/changelog"
129 );
130 expect(res.status).toBe(404);
131 const body = await res.text();
132 expect(body.toLowerCase()).toContain("repository not found");
133 });
134});