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.
| a9ada5f | 1 | /** |
| 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 | ||
| 12 | import { describe, it, expect } from "bun:test"; | |
| 13 | import { | |
| 14 | ISSUE_TRIAGE_MARKER, | |
| 15 | triggerIssueTriage, | |
| 16 | __test, | |
| 17 | } from "../lib/issue-triage"; | |
| 18 | import type { IssueTriage } from "../lib/ai-generators"; | |
| 19 | ||
| 20 | const 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 | ||
| 28 | describe("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 | ||
| 36 | describe("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 | ||
| 97 | describe("__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 | ||
| 121 | describe("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 | }); |