1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { mkdtemp, rm, mkdir, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
const SCRATCH = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-"));
process.env.GIT_REPOS_PATH = SCRATCH;
const { default: aiChangelog } = await import("../routes/ai-changelog");
const OWNER = "alice";
const REPO = "demo";
async function run(cmd: string[], cwd: string) {
const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
await proc.exited;
}
async function seedRepo(): Promise<void> {
const bareDir = join(SCRATCH, OWNER, `${REPO}.git`);
await mkdir(bareDir, { recursive: true });
await run(["git", "init", "--bare", bareDir], SCRATCH);
await run(
["git", "symbolic-ref", "HEAD", "refs/heads/main"],
bareDir
);
const workDir = await mkdtemp(join(tmpdir(), "gluecron-ai-changelog-work-"));
await run(["git", "init", workDir], SCRATCH);
await run(
["git", "-C", workDir, "config", "user.email", "test@example.com"],
SCRATCH
);
await run(
["git", "-C", workDir, "config", "user.name", "Test"],
SCRATCH
);
await writeFile(join(workDir, "README.md"), "# hello\n");
await run(["git", "-C", workDir, "add", "README.md"], SCRATCH);
await run(
["git", "-C", workDir, "commit", "-m", "initial commit"],
SCRATCH
);
await run(
["git", "-C", workDir, "branch", "-M", "main"],
SCRATCH
);
await run(
["git", "-C", workDir, "remote", "add", "origin", bareDir],
SCRATCH
);
await run(
["git", "-C", workDir, "push", "origin", "main"],
SCRATCH
);
await rm(workDir, { recursive: true, force: true });
}
describe("routes/ai-changelog — module", () => {
it("imports cleanly and exports a Hono app", () => {
expect(aiChangelog).toBeDefined();
expect(typeof aiChangelog.request).toBe("function");
});
});
describe("routes/ai-changelog — repo present", () => {
beforeAll(async () => {
await seedRepo();
});
afterAll(async () => {
await rm(SCRATCH, { recursive: true, force: true }).catch(() => {});
});
it("GET without query params renders the picker form", async () => {
const res = await aiChangelog.request(
`/${OWNER}/${REPO}/ai/changelog`
);
expect(res.status).toBe(200);
const body = await res.text();
expect(body).toContain("AI Changelog");
expect(body).toContain('name="from"');
expect(body).toContain('name="to"');
expect(body.toLowerCase()).toContain("generate");
});
it("GET with nonsense from/to renders an error banner, not a 500", async () => {
const res = await aiChangelog.request(
`/${OWNER}/${REPO}/ai/changelog?from=not-a-real-ref-xyz&to=also-not-real-abc`
);
expect(res.status).toBe(200);
const body = await res.text();
expect(body).toContain("auth-error");
expect(body).toMatch(/Could not resolve/i);
});
});
describe("routes/ai-changelog — missing repo", () => {
it("returns 404 for a non-existent repo", async () => {
const res = await aiChangelog.request(
"/nobody-xyz/nothing-xyz/ai/changelog"
);
expect(res.status).toBe(404);
const body = await res.text();
expect(body.toLowerCase()).toContain("repository not found");
});
});
|