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

issue-triage.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-triage.test.tsBlame166 lines · 1 contributor
a9ada5fClaude1/**
2 * Tests for src/lib/issue-triage.ts.
3 *
4 * Pure helper renderIssueTriageComment covered exhaustively. The
5 * DB-touching paths (alreadyTriaged, loadAvailableLabels,
6 * loadRecentIssues) are exercised via the fail-open contracts.
7 *
8 * triggerIssueTriage itself is verified to be a no-throw fire-and-forget
9 * even when called with garbage inputs and no API key.
10 */
11
12import { describe, it, expect } from "bun:test";
13import {
14 ISSUE_TRIAGE_MARKER,
15 triggerIssueTriage,
16 __test,
17} from "../lib/issue-triage";
18import type { IssueTriage } from "../lib/ai-generators";
19
20const triageFixture = (overrides: Partial<IssueTriage> = {}): IssueTriage => ({
21 suggestedLabels: ["bug", "ui"],
22 duplicateOfIssueNumber: null,
23 priority: "medium",
24 summary: "Login button is unreadable in dark mode.",
25 ...overrides,
26});
27
28describe("ISSUE_TRIAGE_MARKER", () => {
29 it("is a stable HTML comment so future searches keep working", () => {
30 expect(ISSUE_TRIAGE_MARKER).toBe("<!-- gluecron-issue-triage:summary -->");
31 expect(ISSUE_TRIAGE_MARKER.startsWith("<!--")).toBe(true);
32 expect(ISSUE_TRIAGE_MARKER.endsWith("-->")).toBe(true);
33 });
34});
35
36describe("renderIssueTriageComment", () => {
37 it("includes the marker, summary, priority, labels", () => {
38 const out = __test.renderIssueTriageComment(triageFixture());
39 expect(out).toContain(ISSUE_TRIAGE_MARKER);
40 expect(out).toContain("## AI Triage");
41 expect(out).toContain("Login button is unreadable in dark mode.");
42 expect(out).toContain("**Priority:** medium");
43 expect(out).toContain("`bug`");
44 expect(out).toContain("`ui`");
45 });
46
47 it("uses an italic placeholder when there are no label suggestions", () => {
48 const out = __test.renderIssueTriageComment(
49 triageFixture({ suggestedLabels: [] })
50 );
51 expect(out).toContain("_(no label suggestions)_");
52 });
53
54 it("uses an italic placeholder when summary is missing/whitespace", () => {
55 const a = __test.renderIssueTriageComment(triageFixture({ summary: "" }));
56 const b = __test.renderIssueTriageComment(triageFixture({ summary: " " }));
57 expect(a).toContain("_(no summary)_");
58 expect(b).toContain("_(no summary)_");
59 });
60
61 it("renders a duplicate callout when AI flagged one", () => {
62 const out = __test.renderIssueTriageComment(
63 triageFixture({ duplicateOfIssueNumber: 42 })
64 );
65 expect(out).toContain("**Possible duplicate of:** #42");
66 });
67
68 it("omits the duplicate callout for null / 0 / negative numbers", () => {
69 const a = __test.renderIssueTriageComment(triageFixture({ duplicateOfIssueNumber: null }));
70 const b = __test.renderIssueTriageComment(
71 triageFixture({ duplicateOfIssueNumber: 0 as any })
72 );
73 const c = __test.renderIssueTriageComment(
74 triageFixture({ duplicateOfIssueNumber: -1 as any })
75 );
76 for (const out of [a, b, c]) {
77 expect(out).not.toContain("Possible duplicate of");
78 }
79 });
80
81 it("ends with the suggestions-only disclaimer", () => {
82 const out = __test.renderIssueTriageComment(triageFixture());
83 expect(out.trimEnd().endsWith(
84 "_Suggestions only — nothing has been applied. The author stays in control._"
85 )).toBe(true);
86 });
87
88 it("formats critical priority literally (no pictographic emoji)", () => {
89 const out = __test.renderIssueTriageComment(
90 triageFixture({ priority: "critical" })
91 );
92 expect(out).toContain("**Priority:** critical");
93 expect(/\p{Extended_Pictographic}/u.test(out)).toBe(false);
94 });
95});
96
97describe("__test fail-open contracts", () => {
98 it("alreadyTriaged returns false when no DB / unknown issue", async () => {
99 const out = await __test.alreadyTriaged(
100 "00000000-0000-0000-0000-000000000000"
101 );
102 expect(out).toBe(false);
103 });
104
105 it("loadAvailableLabels returns [] for an unknown repo", async () => {
106 const out = await __test.loadAvailableLabels(
107 "00000000-0000-0000-0000-000000000000"
108 );
109 expect(Array.isArray(out)).toBe(true);
110 });
111
112 it("loadRecentIssues returns [] for an unknown repo", async () => {
113 const out = await __test.loadRecentIssues(
114 "00000000-0000-0000-0000-000000000000",
115 "00000000-0000-0000-0000-000000000000"
116 );
117 expect(Array.isArray(out)).toBe(true);
118 });
119});
120
121describe("triggerIssueTriage — fire-and-forget never throws", () => {
122 const before = process.env.ANTHROPIC_API_KEY;
123
124 it("resolves without throwing when API key is absent", async () => {
125 delete process.env.ANTHROPIC_API_KEY;
126 try {
127 let threw = false;
128 try {
129 await triggerIssueTriage({
130 ownerName: "alice",
131 repoName: "demo",
132 repositoryId: "00000000-0000-0000-0000-000000000000",
133 issueId: "00000000-0000-0000-0000-000000000000",
134 issueNumber: 1,
135 authorId: "00000000-0000-0000-0000-000000000000",
136 title: "Test",
137 body: "",
138 });
139 } catch {
140 threw = true;
141 }
142 expect(threw).toBe(false);
143 } finally {
144 if (before) process.env.ANTHROPIC_API_KEY = before;
145 }
146 });
147
148 it("never throws on garbage inputs", async () => {
149 let threw = false;
150 try {
151 await triggerIssueTriage({
152 ownerName: "",
153 repoName: "",
154 repositoryId: "not-a-uuid",
155 issueId: "not-a-uuid",
156 issueNumber: -1,
157 authorId: "not-a-uuid",
158 title: "",
159 body: "",
160 });
161 } catch {
162 threw = true;
163 }
164 expect(threw).toBe(false);
165 });
166});