CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
pr-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.
| cb6fd59 | 1 | /** |
| 2 | * Tests for src/lib/pr-triage.ts. | |
| 3 | * | |
| 4 | * Pure helpers (renderTriageComment) covered exhaustively. The | |
| 5 | * DB-touching paths (alreadyTriaged, loadAvailableLabels, | |
| 6 | * loadCandidateReviewers, buildDiffSummary) are exercised via the | |
| 7 | * fail-open contracts — they must return reasonable values when no | |
| 8 | * DB / no repo on disk, never throw. | |
| 9 | * | |
| 10 | * triggerPrTriage itself is verified to be a no-throw fire-and-forget | |
| 11 | * even when called with garbage inputs and no API key. | |
| 12 | */ | |
| 13 | ||
| 14 | import { describe, it, expect } from "bun:test"; | |
| 15 | import { | |
| 16 | PR_TRIAGE_MARKER, | |
| 17 | triggerPrTriage, | |
| 18 | __test, | |
| 19 | } from "../lib/pr-triage"; | |
| 20 | import type { PrTriage } from "../lib/ai-generators"; | |
| 21 | ||
| 22 | const triageFixture = (overrides: Partial<PrTriage> = {}): PrTriage => ({ | |
| 23 | suggestedLabels: ["bug", "ui"], | |
| 24 | suggestedReviewerUsernames: ["alice", "bob"], | |
| 25 | priority: "medium", | |
| 26 | riskArea: "frontend", | |
| 27 | summary: "Fix login form colour contrast.", | |
| 28 | ...overrides, | |
| 29 | }); | |
| 30 | ||
| 31 | describe("PR_TRIAGE_MARKER", () => { | |
| 32 | it("is a stable HTML comment so future searches keep working", () => { | |
| 33 | expect(PR_TRIAGE_MARKER).toBe("<!-- gluecron-pr-triage:summary -->"); | |
| 34 | expect(PR_TRIAGE_MARKER.startsWith("<!--")).toBe(true); | |
| 35 | expect(PR_TRIAGE_MARKER.endsWith("-->")).toBe(true); | |
| 36 | }); | |
| 37 | }); | |
| 38 | ||
| 39 | describe("renderTriageComment", () => { | |
| 40 | it("includes the marker, summary, priority, risk area, labels, reviewers", () => { | |
| 41 | const out = __test.renderTriageComment(triageFixture()); | |
| 42 | expect(out).toContain(PR_TRIAGE_MARKER); | |
| 43 | expect(out).toContain("## AI Triage"); | |
| 44 | expect(out).toContain("Fix login form colour contrast."); | |
| 45 | expect(out).toContain("**Priority:** medium"); | |
| 46 | expect(out).toContain("**Risk area:** frontend"); | |
| 47 | expect(out).toContain("`bug`"); | |
| 48 | expect(out).toContain("`ui`"); | |
| 49 | expect(out).toContain("@alice"); | |
| 50 | expect(out).toContain("@bob"); | |
| 51 | }); | |
| 52 | ||
| 53 | it("uses an italic placeholder when there are no label suggestions", () => { | |
| 54 | const out = __test.renderTriageComment(triageFixture({ suggestedLabels: [] })); | |
| 55 | expect(out).toContain("_(no label suggestions)_"); | |
| 56 | }); | |
| 57 | ||
| 58 | it("uses an italic placeholder when there are no reviewer suggestions", () => { | |
| 59 | const out = __test.renderTriageComment( | |
| 60 | triageFixture({ suggestedReviewerUsernames: [] }) | |
| 61 | ); | |
| 62 | expect(out).toContain("_(no reviewer suggestions)_"); | |
| 63 | }); | |
| 64 | ||
| 65 | it("renders an italic placeholder when summary is missing/whitespace", () => { | |
| 66 | const a = __test.renderTriageComment(triageFixture({ summary: "" })); | |
| 67 | const b = __test.renderTriageComment(triageFixture({ summary: " " })); | |
| 68 | expect(a).toContain("_(no summary)_"); | |
| 69 | expect(b).toContain("_(no summary)_"); | |
| 70 | }); | |
| 71 | ||
| 72 | it("ends with the suggestions-only disclaimer", () => { | |
| 73 | const out = __test.renderTriageComment(triageFixture()); | |
| 74 | expect(out.trimEnd().endsWith( | |
| 75 | "_Suggestions only — nothing has been applied. The PR author stays in control._" | |
| 76 | )).toBe(true); | |
| 77 | }); | |
| 78 | ||
| 79 | it("formats critical priority literally (no pictographic emoji)", () => { | |
| 80 | // Reminder: tests guard the "no emoji" rule. We use the tighter | |
| 81 | // Extended_Pictographic class so legitimate punctuation (em-dash, | |
| 82 | // asterisks) doesn't match. | |
| 83 | const out = __test.renderTriageComment( | |
| 84 | triageFixture({ priority: "critical" }) | |
| 85 | ); | |
| 86 | expect(out).toContain("**Priority:** critical"); | |
| 87 | expect(/\p{Extended_Pictographic}/u.test(out)).toBe(false); | |
| 88 | }); | |
| 89 | }); | |
| 90 | ||
| 91 | describe("__test fail-open contracts", () => { | |
| 92 | it("alreadyTriaged returns false when no DB / unknown PR", async () => { | |
| 93 | const out = await __test.alreadyTriaged( | |
| 94 | "00000000-0000-0000-0000-000000000000" | |
| 95 | ); | |
| 96 | expect(out).toBe(false); | |
| 97 | }); | |
| 98 | ||
| 99 | it("loadAvailableLabels returns [] for an unknown repo", async () => { | |
| 100 | const out = await __test.loadAvailableLabels( | |
| 101 | "00000000-0000-0000-0000-000000000000" | |
| 102 | ); | |
| 103 | expect(Array.isArray(out)).toBe(true); | |
| 104 | }); | |
| 105 | ||
| 106 | it("loadCandidateReviewers returns [] for an unknown repo", async () => { | |
| 107 | const out = await __test.loadCandidateReviewers( | |
| 108 | "00000000-0000-0000-0000-000000000000", | |
| 109 | "00000000-0000-0000-0000-000000000000" | |
| 110 | ); | |
| 111 | expect(Array.isArray(out)).toBe(true); | |
| 112 | }); | |
| 113 | ||
| 114 | it("buildDiffSummary returns '' for an unknown repo", async () => { | |
| 115 | const out = await __test.buildDiffSummary( | |
| 116 | "definitely-not-a-real-owner", | |
| 117 | "definitely-not-a-real-repo", | |
| 118 | "main", | |
| 119 | "feature" | |
| 120 | ); | |
| 121 | expect(out).toBe(""); | |
| 122 | }); | |
| 123 | }); | |
| 124 | ||
| 125 | describe("triggerPrTriage — fire-and-forget never throws", () => { | |
| 126 | const before = process.env.ANTHROPIC_API_KEY; | |
| 127 | ||
| 128 | it("returns cleanly when API key is absent", async () => { | |
| 129 | delete process.env.ANTHROPIC_API_KEY; | |
| 130 | try { | |
| 131 | let threw = false; | |
| 132 | try { | |
| 133 | await triggerPrTriage({ | |
| 134 | ownerName: "alice", | |
| 135 | repoName: "demo", | |
| 136 | repositoryId: "00000000-0000-0000-0000-000000000000", | |
| 137 | prId: "00000000-0000-0000-0000-000000000000", | |
| 138 | prAuthorId: "00000000-0000-0000-0000-000000000000", | |
| 139 | title: "Test", | |
| 140 | body: "", | |
| 141 | baseBranch: "main", | |
| 142 | headBranch: "feature", | |
| 143 | }); | |
| 144 | } catch { | |
| 145 | threw = true; | |
| 146 | } | |
| 147 | expect(threw).toBe(false); | |
| 148 | } finally { | |
| 149 | if (before) process.env.ANTHROPIC_API_KEY = before; | |
| 150 | } | |
| 151 | }); | |
| 152 | ||
| 153 | it("never throws on empty/garbage inputs", async () => { | |
| 154 | let threw = false; | |
| 155 | try { | |
| 156 | await triggerPrTriage({ | |
| 157 | ownerName: "", | |
| 158 | repoName: "", | |
| 159 | repositoryId: "not-a-uuid", | |
| 160 | prId: "not-a-uuid", | |
| 161 | prAuthorId: "not-a-uuid", | |
| 162 | title: "", | |
| 163 | body: "", | |
| 164 | baseBranch: "", | |
| 165 | headBranch: "", | |
| 166 | }); | |
| 167 | } catch { | |
| 168 | threw = true; | |
| 169 | } | |
| 170 | expect(threw).toBe(false); | |
| 171 | }); | |
| 172 | }); |