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

issue-templates.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.

issue-templates.test.tsBlame242 lines · 1 contributor
9a4eb42Claude1/**
2 * Block J17 — Multi-template issue selector. Pure parser unit tests + a
3 * route-auth smoke to make sure the picker route is wired up.
4 */
5
6import { describe, it, expect } from "bun:test";
7import app from "../app";
8import {
9 splitFrontmatter,
10 parseFrontmatterMeta,
11 slugFromFilename,
12 buildTemplateFromFile,
13 findTemplateBySlug,
14 __internal,
15 type IssueTemplate,
16} from "../lib/issue-templates";
17
18describe("issue-templates — splitFrontmatter", () => {
19 it("returns body as-is when there's no frontmatter", () => {
20 const r = splitFrontmatter("hello world");
21 expect(r.frontmatter).toBeNull();
22 expect(r.body).toBe("hello world");
23 });
24
25 it("splits the standard ---\\n...\\n--- fence", () => {
26 const content = "---\nname: Bug\nabout: Report\n---\nSteps to reproduce";
27 const r = splitFrontmatter(content);
28 expect(r.frontmatter).toBe("name: Bug\nabout: Report");
29 expect(r.body).toBe("Steps to reproduce");
30 });
31
32 it("returns body as-is when fence is unterminated", () => {
33 const content = "---\nname: Bug\nno closing fence";
34 const r = splitFrontmatter(content);
35 expect(r.frontmatter).toBeNull();
36 expect(r.body).toBe(content);
37 });
38
39 it("tolerates trailing whitespace on the closing fence line", () => {
40 const content = "---\nname: Bug\n--- \nBody here";
41 const r = splitFrontmatter(content);
42 expect(r.frontmatter).toBe("name: Bug");
43 expect(r.body).toBe("Body here");
44 });
45
46 it("handles CRLF line endings", () => {
47 const content = "---\r\nname: Bug\r\n---\r\nBody";
48 const r = splitFrontmatter(content);
49 // splitFrontmatter itself doesn't normalise CRLF in the split logic; the
50 // parser normalises internally. We just need a usable body when the
51 // frontmatter can't be matched to still roundtrip the raw content.
52 expect(r.body.trim().length).toBeGreaterThan(0);
53 });
54});
55
56describe("issue-templates — parseFrontmatterMeta", () => {
57 it("returns an empty meta for empty input", () => {
58 const m = parseFrontmatterMeta("");
59 expect(m.name).toBeNull();
60 expect(m.about).toBeNull();
61 expect(m.title).toBeNull();
62 expect(m.labels).toEqual([]);
63 expect(m.assignees).toEqual([]);
64 });
65
66 it("parses flat key: value pairs", () => {
67 const m = parseFrontmatterMeta(
68 [
69 "name: Bug report",
70 "about: Something's broken",
71 "title: '[Bug] '",
72 ].join("\n")
73 );
74 expect(m.name).toBe("Bug report");
75 expect(m.about).toBe("Something's broken");
76 expect(m.title).toBe("[Bug] ");
77 });
78
79 it("strips double- and single-quoted values", () => {
80 const m = parseFrontmatterMeta(
81 ['name: "Quoted"', "about: 'single'"].join("\n")
82 );
83 expect(m.name).toBe("Quoted");
84 expect(m.about).toBe("single");
85 });
86
87 it("parses flow-list labels: [a, \"b\", c]", () => {
88 const m = parseFrontmatterMeta('labels: [bug, "high priority", triage]');
89 expect(m.labels).toEqual(["bug", "high priority", "triage"]);
90 });
91
92 it("parses block-list labels (- bug / - triage)", () => {
93 const m = parseFrontmatterMeta(
94 ["labels:", " - bug", " - triage", " - needs-repro"].join("\n")
95 );
96 expect(m.labels).toEqual(["bug", "triage", "needs-repro"]);
97 });
98
99 it("parses block-list assignees", () => {
100 const m = parseFrontmatterMeta(
101 ["assignees:", " - alice", " - bob"].join("\n")
102 );
103 expect(m.assignees).toEqual(["alice", "bob"]);
104 });
105
106 it("ignores comment lines and blanks", () => {
107 const m = parseFrontmatterMeta(
108 ["# a comment", "", "name: X", "# tail"].join("\n")
109 );
110 expect(m.name).toBe("X");
111 });
112
113 it("is case-insensitive on keys", () => {
114 const m = parseFrontmatterMeta("Name: Casey\nLABELS: [x]");
115 expect(m.name).toBe("Casey");
116 expect(m.labels).toEqual(["x"]);
117 });
118
119 it("ignores unknown keys", () => {
120 const m = parseFrontmatterMeta("name: A\ncolor: ff0000");
121 expect(m.name).toBe("A");
122 expect(m.labels).toEqual([]);
123 });
124});
125
126describe("issue-templates — slugFromFilename", () => {
127 it("lowercases and normalises to hyphens", () => {
128 expect(slugFromFilename("Bug Report.md")).toBe("bug-report");
129 expect(slugFromFilename("Feature_Request.md")).toBe("feature-request");
130 });
131
132 it("strips the .md / .markdown / .yml extensions", () => {
133 expect(slugFromFilename("x.md")).toBe("x");
134 expect(slugFromFilename("x.yml")).toBe("x");
135 expect(slugFromFilename("x.yaml")).toBe("x");
136 });
137
138 it("clamps to 64 characters", () => {
139 const base = "a".repeat(100);
140 expect(slugFromFilename(`${base}.md`).length).toBe(64);
141 });
142
143 it("strips unicode and collapses non-alnum runs", () => {
144 expect(slugFromFilename(" !!! bug 😀 .md")).toBe("bug");
145 });
146
147 it("returns '' for an all-unicode name (caller picks fallback)", () => {
148 expect(slugFromFilename("😀😀.md")).toBe("");
149 });
150});
151
152describe("issue-templates — buildTemplateFromFile", () => {
153 it("merges filename + frontmatter + body", () => {
154 const content = [
155 "---",
156 "name: Bug",
157 "about: Report a bug",
158 "title: '[BUG] '",
159 "labels: [bug, triage]",
160 "---",
161 "## Steps",
162 "1. foo",
163 ].join("\n");
164 const t = buildTemplateFromFile("bug.md", content, ".github/ISSUE_TEMPLATE");
165 expect(t.slug).toBe("bug");
166 expect(t.path).toBe(".github/ISSUE_TEMPLATE/bug.md");
167 expect(t.name).toBe("Bug");
168 expect(t.about).toBe("Report a bug");
169 expect(t.title).toBe("[BUG] ");
170 expect(t.labels).toEqual(["bug", "triage"]);
171 expect(t.body).toBe("## Steps\n1. foo");
172 });
173
174 it("falls back to the filename (sans extension) when name is missing", () => {
175 const content = "---\nabout: X\n---\nbody";
176 const t = buildTemplateFromFile("feature-request.md", content, ".github");
177 expect(t.name).toBe("feature-request");
178 });
179
180 it("handles files without frontmatter", () => {
181 const t = buildTemplateFromFile("plain.md", "Just a body\n", ".github");
182 expect(t.name).toBe("plain");
183 expect(t.about).toBeNull();
184 expect(t.labels).toEqual([]);
185 expect(t.body).toBe("Just a body");
186 });
187
188 it("joins empty dir path without a leading slash", () => {
189 const t = buildTemplateFromFile("x.md", "hi", "");
190 expect(t.path).toBe("x.md");
191 });
192});
193
194describe("issue-templates — findTemplateBySlug", () => {
195 const items: IssueTemplate[] = [
196 buildTemplateFromFile("bug.md", "---\nname: Bug\n---\n", ".github"),
197 buildTemplateFromFile("feature.md", "---\nname: Feature\n---\n", ".github"),
198 ];
199
200 it("returns null for null/undefined slugs", () => {
201 expect(findTemplateBySlug(items, null)).toBeNull();
202 expect(findTemplateBySlug(items, undefined)).toBeNull();
203 expect(findTemplateBySlug(items, "")).toBeNull();
204 });
205
206 it("returns null for an unknown slug", () => {
207 expect(findTemplateBySlug(items, "missing")).toBeNull();
208 });
209
210 it("finds by exact slug", () => {
211 expect(findTemplateBySlug(items, "bug")?.name).toBe("Bug");
212 expect(findTemplateBySlug(items, "feature")?.name).toBe("Feature");
213 });
214});
215
216describe("issue-templates — __internal", () => {
217 it("exposes the pure helpers for parity", () => {
218 expect(__internal.splitFrontmatter).toBe(splitFrontmatter);
219 expect(__internal.parseFrontmatterMeta).toBe(parseFrontmatterMeta);
220 expect(__internal.slugFromFilename).toBe(slugFromFilename);
221 expect(__internal.buildTemplateFromFile).toBe(buildTemplateFromFile);
222 expect(__internal.findTemplateBySlug).toBe(findTemplateBySlug);
223 expect(__internal.TEMPLATE_DIRS.length).toBeGreaterThan(0);
224 expect(typeof __internal.MAX_TEMPLATE_BYTES).toBe("number");
225 expect(typeof __internal.MAX_TEMPLATES).toBe("number");
226 });
227});
228
229describe("issue-templates — routes", () => {
230 it("GET /:owner/:repo/issues/new requires auth", async () => {
231 const res = await app.request("/alice/nope/issues/new");
232 // redirect to login OR 404 from repo resolve OR 401 JSON path
233 expect([302, 401, 404].includes(res.status)).toBe(true);
234 });
235
236 it("GET /:owner/:repo/issues/new?template=foo requires auth", async () => {
237 const res = await app.request(
238 "/alice/nope/issues/new?template=foo"
239 );
240 expect([302, 401, 404].includes(res.status)).toBe(true);
241 });
242});