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

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.

pr-triage.test.tsBlame172 lines · 1 contributor
cb6fd59Claude1/**
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
14import { describe, it, expect } from "bun:test";
15import {
16 PR_TRIAGE_MARKER,
17 triggerPrTriage,
18 __test,
19} from "../lib/pr-triage";
20import type { PrTriage } from "../lib/ai-generators";
21
22const 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
31describe("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
39describe("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
91describe("__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
125describe("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});