This commit is contained in:
evanpelle
2026-05-30 19:49:37 -07:00
parent 885e31460c
commit 168db85be5
20 changed files with 4181 additions and 1 deletions
@@ -0,0 +1,174 @@
import { describe, expect, it } from "vitest";
import type { DigestInputs } from "../claude";
import {
ageDays,
formatIsoDate,
renderFallbackDigest,
truncateForDiscord,
} from "../digest-formatter";
function emptyInputs(): DigestInputs {
return {
today: "2026-05-29",
triageReadyInWindow: [],
needsMaintainerTriage: [],
needsInfoCount: 0,
staleClosingSoon: [],
awaitingMilestone: [],
prReviews: [],
triageSummary: { processed: 0, closed: 0, kept: 0 },
};
}
describe("formatIsoDate", () => {
it("renders YYYY-MM-DD", () => {
expect(formatIsoDate(new Date("2026-05-29T12:34:56Z"))).toBe("2026-05-29");
});
});
describe("renderFallbackDigest", () => {
it("includes only sections with content", () => {
const out = renderFallbackDigest(emptyInputs());
expect(out).toContain("# Daily Triage — 2026-05-29");
expect(out).not.toContain("🟢 Newly classified");
expect(out).not.toContain("🟡 Needs your judgment");
expect(out).not.toContain("🔴 Auto-closing");
expect(out).not.toContain("🟠 Awaiting your milestone");
expect(out).not.toContain("🔵 PRs needing review");
});
it("emits the newly-classified section with each issue", () => {
const out = renderFallbackDigest({
...emptyInputs(),
triageReadyInWindow: [
{
number: 100,
title: "Crash on right-click",
author: "alice",
area: "area:client",
},
{ number: 101, title: "Hotkey for chat", author: "bob", area: null },
],
});
expect(out).toContain("🟢 Newly classified by Claude (2)");
expect(out).toContain(
"#100 [area:client] — Crash on right-click (by @alice)",
);
expect(out).toContain("#101 — Hotkey for chat (by @bob)");
});
it("emits the needs-judgment section with reasoning when present", () => {
const out = renderFallbackDigest({
...emptyInputs(),
needsMaintainerTriage: [
{ number: 5, title: "ambiguous report", reasoning: "could be PEBKAC" },
{ number: 6, title: "no reasoning case" },
],
});
expect(out).toContain("🟡 Needs your judgment (2)");
expect(out).toContain("#5 — ambiguous report _(could be PEBKAC)_");
expect(out).toContain("#6 — no reasoning case");
});
it("emits the awaiting-milestone section with all entries, tags, and age", () => {
const out = renderFallbackDigest({
...emptyInputs(),
awaitingMilestone: [
{
number: 45,
title: "Crash on right-click",
ageDays: 21,
primaryLabel: "bug",
area: "area:client",
},
{
number: 76,
title: "Add chat hotkey",
ageDays: 12,
primaryLabel: "qol-improvement",
area: null,
},
{
number: 99,
title: "Just opened",
ageDays: 0,
primaryLabel: null,
area: null,
},
],
});
expect(out).toContain("🟠 Awaiting your milestone (3)");
expect(out).toContain(
"#45 [bug, area:client] — 21d — Crash on right-click",
);
expect(out).toContain("#76 [qol-improvement] — 12d — Add chat hotkey");
expect(out).toContain("#99 — 0d — Just opened");
});
it("emits the PR review section with risk/tests/scope summary", () => {
const out = renderFallbackDigest({
...emptyInputs(),
prReviews: [
{
pr: { number: 220, title: "tick refactor", author: "carol" },
review: {
summary: "refactors tick loop",
risk: "high",
tests_present: "yes",
scope_match: "matches",
},
},
],
});
expect(out).toContain("🔵 PRs needing review (1)");
expect(out).toContain(
"#220 by @carol — refactors tick loop · risk: high · tests: yes · scope: matches",
);
});
it("appends a needs-info count when nonzero", () => {
const out = renderFallbackDigest({ ...emptyInputs(), needsInfoCount: 4 });
expect(out).toContain("4 issues waiting on reporter");
});
it("omits needs-info line when count is zero", () => {
expect(renderFallbackDigest(emptyInputs())).not.toContain(
"waiting on reporter",
);
});
it("always tags itself as the fallback", () => {
expect(renderFallbackDigest(emptyInputs())).toContain("Fallback digest");
});
});
describe("ageDays", () => {
it("rounds down to whole days", () => {
const now = new Date("2026-05-30T12:00:00Z");
expect(ageDays("2026-05-30T11:00:00Z", now)).toBe(0);
expect(ageDays("2026-05-29T11:00:00Z", now)).toBe(1);
expect(ageDays("2026-05-09T12:00:00Z", now)).toBe(21);
});
it("returns 0 for future-dated input (clock skew)", () => {
const now = new Date("2026-05-30T12:00:00Z");
expect(ageDays("2026-06-01T00:00:00Z", now)).toBe(0);
});
it("returns 0 for unparseable input", () => {
expect(ageDays("not-a-date", new Date())).toBe(0);
});
});
describe("truncateForDiscord", () => {
it("returns the input unchanged when under the limit", () => {
expect(truncateForDiscord("short")).toBe("short");
});
it("truncates and appends a marker when over the limit", () => {
const long = "a".repeat(2000);
const out = truncateForDiscord(long, 100);
expect(out.endsWith("…(truncated)")).toBe(true);
expect(out.length).toBeLessThan(long.length);
});
});
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { FORBIDDEN_LABELS } from "../config";
import { assertNotForbidden, type Action } from "../github";
describe("assertNotForbidden", () => {
it("allows non-forbidden labels", () => {
expect(() =>
assertNotForbidden({ type: "add_label", label: "bug" }),
).not.toThrow();
expect(() =>
assertNotForbidden({ type: "remove_label", label: "needs-info" }),
).not.toThrow();
});
it("throws when trying to add a Layer A label", () => {
for (const forbidden of FORBIDDEN_LABELS) {
expect(() =>
assertNotForbidden({ type: "add_label", label: forbidden }),
).toThrow(/forbidden label/);
}
});
it("throws when trying to remove a Layer A label", () => {
for (const forbidden of FORBIDDEN_LABELS) {
expect(() =>
assertNotForbidden({ type: "remove_label", label: forbidden }),
).toThrow(/forbidden label/);
}
});
it("does not throw for comment / close actions", () => {
const actions: Action[] = [
{ type: "comment", body: "hello" },
{ type: "close", reason: "not_planned" },
];
for (const a of actions) {
expect(() => assertNotForbidden(a)).not.toThrow();
}
});
it("FORBIDDEN_LABELS contains exactly the Layer A labels", () => {
expect(FORBIDDEN_LABELS.has("approved")).toBe(true);
expect(FORBIDDEN_LABELS.has("not-approved")).toBe(true);
expect(FORBIDDEN_LABELS.has("stale")).toBe(true);
expect(FORBIDDEN_LABELS.has("keep-open")).toBe(true);
expect(FORBIDDEN_LABELS.has("auto-closed-stale")).toBe(true);
expect(FORBIDDEN_LABELS.has("bug")).toBe(false);
expect(FORBIDDEN_LABELS.has("needs-info")).toBe(false);
});
});
@@ -0,0 +1,363 @@
import { describe, expect, it } from "vitest";
import type { TriageDecision } from "../claude";
import { ENABLE_TRANSLATION_CLOSE, LABELS } from "../config";
import type { Issue } from "../github";
import {
classificationClosesIssue,
decisionToActions,
downgradeIfLowConfidence,
isOrgMemberIssueNeedingTag,
isTriageCandidate,
} from "../pass1-triage";
function makeIssue(overrides: Partial<Issue> = {}): Issue {
return {
number: 1,
title: "Test issue",
body: "Test body",
state: "open",
labels: [],
user: { login: "alice" },
author_association: "NONE",
created_at: "2026-05-29T00:00:00Z",
updated_at: "2026-05-29T00:00:00Z",
is_pull_request: false,
...overrides,
};
}
describe("isTriageCandidate", () => {
it("accepts a clean open issue from an outside contributor", () => {
expect(isTriageCandidate(makeIssue())).toBe(true);
});
it("rejects pull requests", () => {
expect(isTriageCandidate(makeIssue({ is_pull_request: true }))).toBe(false);
});
it("rejects closed issues", () => {
expect(isTriageCandidate(makeIssue({ state: "closed" }))).toBe(false);
});
it("rejects already-triaged issues", () => {
expect(
isTriageCandidate(makeIssue({ labels: [LABELS.CLAUDE_TRIAGED] })),
).toBe(false);
});
it("rejects issues from org members", () => {
for (const assoc of ["OWNER", "MEMBER", "COLLABORATOR"]) {
expect(isTriageCandidate(makeIssue({ author_association: assoc }))).toBe(
false,
);
}
});
it("rejects issues that have any auto-closed-* label", () => {
expect(
isTriageCandidate(makeIssue({ labels: ["auto-closed-feature"] })),
).toBe(false);
expect(
isTriageCandidate(makeIssue({ labels: ["auto-closed-support"] })),
).toBe(false);
});
});
describe("isOrgMemberIssueNeedingTag", () => {
it("flags org-member issues without claude-triaged", () => {
expect(
isOrgMemberIssueNeedingTag(makeIssue({ author_association: "MEMBER" })),
).toBe(true);
});
it("does not flag org-member issues already tagged", () => {
expect(
isOrgMemberIssueNeedingTag(
makeIssue({
author_association: "MEMBER",
labels: [LABELS.CLAUDE_TRIAGED],
}),
),
).toBe(false);
});
it("does not flag outside-contributor issues", () => {
expect(
isOrgMemberIssueNeedingTag(makeIssue({ author_association: "NONE" })),
).toBe(false);
});
});
describe("downgradeIfLowConfidence", () => {
const cases: Array<{
classification: TriageDecision["classification"];
confidence: TriageDecision["confidence"];
expectDowngrade: boolean;
}> = [
{ classification: "feature", confidence: "low", expectDowngrade: true },
{ classification: "feature", confidence: "medium", expectDowngrade: true },
{ classification: "feature", confidence: "high", expectDowngrade: false },
{ classification: "question", confidence: "medium", expectDowngrade: true },
{ classification: "support", confidence: "low", expectDowngrade: true },
{ classification: "billing", confidence: "high", expectDowngrade: false },
{ classification: "security", confidence: "low", expectDowngrade: false },
{ classification: "security", confidence: "high", expectDowngrade: false },
{
classification: "translation",
confidence: "medium",
expectDowngrade: true,
},
{ classification: "bug", confidence: "low", expectDowngrade: false },
{
classification: "qol-improvement",
confidence: "low",
expectDowngrade: false,
},
{
classification: "needs-info",
confidence: "medium",
expectDowngrade: false,
},
{ classification: "duplicate", confidence: "low", expectDowngrade: false },
{ classification: "uncertain", confidence: "low", expectDowngrade: false },
];
for (const c of cases) {
it(`${c.classification} @ ${c.confidence}${c.expectDowngrade ? "downgrade" : "keep"}`, () => {
const input: TriageDecision = {
classification: c.classification,
confidence: c.confidence,
reasoning: "test",
};
const out = downgradeIfLowConfidence(input);
if (c.expectDowngrade) {
expect(out.classification).toBe("uncertain");
expect(out.reasoning).toContain("downgraded");
} else {
expect(out).toBe(input);
}
});
}
});
describe("decisionToActions", () => {
it("bug → bug + area + claude-approved + claude-triaged", () => {
const actions = decisionToActions(
{
classification: "bug",
confidence: "high",
reasoning: "x",
suggested_area: "area:client",
},
makeIssue(),
);
const labelsAdded = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labelsAdded).toEqual([
LABELS.BUG,
"area:client",
LABELS.CLAUDE_APPROVED,
LABELS.CLAUDE_TRIAGED,
]);
expect(actions.find((a) => a.type === "close")).toBeUndefined();
expect(actions.find((a) => a.type === "comment")).toBeUndefined();
});
it("bug without area still claude-approved and triaged", () => {
const actions = decisionToActions(
{ classification: "bug", confidence: "high", reasoning: "x" },
makeIssue(),
);
const labelsAdded = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labelsAdded).toEqual([
LABELS.BUG,
LABELS.CLAUDE_APPROVED,
LABELS.CLAUDE_TRIAGED,
]);
});
it("qol-improvement mirrors bug structure with qol label", () => {
const actions = decisionToActions(
{
classification: "qol-improvement",
confidence: "high",
reasoning: "x",
suggested_area: "area:core",
},
makeIssue(),
);
const labelsAdded = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labelsAdded).toEqual([
LABELS.QOL_IMPROVEMENT,
"area:core",
LABELS.CLAUDE_APPROVED,
LABELS.CLAUDE_TRIAGED,
]);
});
it("duplicate adds possible-duplicate, posts dup comment, does NOT close", () => {
const actions = decisionToActions(
{
classification: "duplicate",
confidence: "high",
reasoning: "x",
duplicate_of: 42,
},
makeIssue(),
);
const labels = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labels).toContain(LABELS.POSSIBLE_DUPLICATE);
expect(labels).toContain(LABELS.CLAUDE_TRIAGED);
const comment = actions.find((a) => a.type === "comment");
expect(comment).toBeDefined();
expect((comment as { body: string }).body).toContain("#42");
expect(actions.find((a) => a.type === "close")).toBeUndefined();
});
it("needs-info includes only the questions Claude provided", () => {
const actions = decisionToActions(
{
classification: "needs-info",
confidence: "high",
reasoning: "x",
clarifying_questions: ["Which browser?", "What did you click?"],
},
makeIssue(),
);
const comment = actions.find((a) => a.type === "comment");
expect(comment).toBeDefined();
expect((comment as { body: string }).body).toContain("Which browser?");
expect((comment as { body: string }).body).toContain("What did you click?");
});
it("needs-info with no questions adds label but skips comment", () => {
const actions = decisionToActions(
{ classification: "needs-info", confidence: "high", reasoning: "x" },
makeIssue(),
);
expect(actions.find((a) => a.type === "comment")).toBeUndefined();
});
it("each auto-close classification adds correct label, comment, close", () => {
const expectations: Array<{
cls: TriageDecision["classification"];
label: string;
}> = [
{ cls: "feature", label: LABELS.AUTO_CLOSED_FEATURE },
{ cls: "question", label: LABELS.AUTO_CLOSED_QUESTION },
{ cls: "support", label: LABELS.AUTO_CLOSED_SUPPORT },
{ cls: "billing", label: LABELS.AUTO_CLOSED_BILLING },
];
for (const { cls, label } of expectations) {
const actions = decisionToActions(
{ classification: cls, confidence: "high", reasoning: "x" },
makeIssue(),
);
const labels = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labels).toContain(label);
expect(labels).toContain(LABELS.CLAUDE_TRIAGED);
expect(actions.find((a) => a.type === "comment")).toBeDefined();
expect(actions.find((a) => a.type === "close")).toBeDefined();
}
});
it("security is never auto-closed; routes to claude-uncertain (no comment, no close)", () => {
const actions = decisionToActions(
{ classification: "security", confidence: "high", reasoning: "x" },
makeIssue(),
);
const labels = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labels).toEqual([LABELS.CLAUDE_UNCERTAIN, LABELS.CLAUDE_TRIAGED]);
expect(actions.find((a) => a.type === "close")).toBeUndefined();
expect(actions.find((a) => a.type === "comment")).toBeUndefined();
});
it("translation auto-closes only when ENABLE_TRANSLATION_CLOSE is true", () => {
const actions = decisionToActions(
{ classification: "translation", confidence: "high", reasoning: "x" },
makeIssue(),
);
if (ENABLE_TRANSLATION_CLOSE) {
expect(actions.find((a) => a.type === "close")).toBeDefined();
} else {
expect(actions.find((a) => a.type === "close")).toBeUndefined();
const labels = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labels).toContain(LABELS.CLAUDE_UNCERTAIN);
}
});
it("uncertain adds claude-uncertain and claude-triaged only", () => {
const actions = decisionToActions(
{ classification: "uncertain", confidence: "low", reasoning: "x" },
makeIssue(),
);
const labels = actions
.filter((a) => a.type === "add_label")
.map((a) => (a as { label: string }).label);
expect(labels).toEqual([LABELS.CLAUDE_UNCERTAIN, LABELS.CLAUDE_TRIAGED]);
expect(actions.find((a) => a.type === "close")).toBeUndefined();
expect(actions.find((a) => a.type === "comment")).toBeUndefined();
});
it("always ends with the claude-triaged label", () => {
const all: TriageDecision["classification"][] = [
"bug",
"qol-improvement",
"duplicate",
"needs-info",
"feature",
"question",
"support",
"billing",
"security",
"translation",
"uncertain",
];
for (const cls of all) {
const actions = decisionToActions(
{ classification: cls, confidence: "high", reasoning: "x" },
makeIssue(),
);
const lastAddLabel = [...actions]
.reverse()
.find((a) => a.type === "add_label");
expect((lastAddLabel as { label: string }).label).toBe(
LABELS.CLAUDE_TRIAGED,
);
}
});
});
describe("classificationClosesIssue", () => {
it("auto-close categories return true (translation depends on flag)", () => {
expect(classificationClosesIssue("feature")).toBe(true);
expect(classificationClosesIssue("question")).toBe(true);
expect(classificationClosesIssue("support")).toBe(true);
expect(classificationClosesIssue("billing")).toBe(true);
expect(classificationClosesIssue("translation")).toBe(
ENABLE_TRANSLATION_CLOSE,
);
});
it("non-close categories return false (including security)", () => {
expect(classificationClosesIssue("bug")).toBe(false);
expect(classificationClosesIssue("qol-improvement")).toBe(false);
expect(classificationClosesIssue("duplicate")).toBe(false);
expect(classificationClosesIssue("needs-info")).toBe(false);
expect(classificationClosesIssue("uncertain")).toBe(false);
expect(classificationClosesIssue("security")).toBe(false);
});
});
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import {
extractLinkedIssueNumber,
isReviewPendingFromReviews,
} from "../pr-reviewer";
describe("extractLinkedIssueNumber", () => {
it("matches 'Fixes #123'", () => {
expect(extractLinkedIssueNumber("Fixes #123")).toBe(123);
});
it("matches 'closes #45'", () => {
expect(extractLinkedIssueNumber("This PR closes #45 by adding...")).toBe(
45,
);
});
it("matches 'Resolves #99'", () => {
expect(extractLinkedIssueNumber("Summary\n\nResolves #99")).toBe(99);
});
it("matches 'fixed #7'", () => {
expect(extractLinkedIssueNumber("fixed #7")).toBe(7);
});
it("prefers the first match when multiple keywords are present", () => {
expect(extractLinkedIssueNumber("Fixes #10\n\nAlso closes #20")).toBe(10);
});
it("returns null when no linking keyword is found", () => {
expect(extractLinkedIssueNumber("Just a refactor")).toBeNull();
expect(
extractLinkedIssueNumber("Reference to #50 but no keyword"),
).toBeNull();
});
it("returns null for empty body", () => {
expect(extractLinkedIssueNumber("")).toBeNull();
});
});
describe("isReviewPendingFromReviews", () => {
const HEAD = "abc123";
const OLD = "def456";
it("pending when maintainer has no reviews at all", () => {
expect(isReviewPendingFromReviews([], "evan", HEAD)).toBe(true);
});
it("pending when only OTHER reviewers have reviewed the head", () => {
const reviews = [{ user_login: "alice", commit_id: HEAD }];
expect(isReviewPendingFromReviews(reviews, "evan", HEAD)).toBe(true);
});
it("pending when maintainer reviewed an OLD commit (new commits pushed)", () => {
const reviews = [{ user_login: "evan", commit_id: OLD }];
expect(isReviewPendingFromReviews(reviews, "evan", HEAD)).toBe(true);
});
it("not pending when maintainer has reviewed the current head", () => {
const reviews = [
{ user_login: "evan", commit_id: OLD },
{ user_login: "evan", commit_id: HEAD },
];
expect(isReviewPendingFromReviews(reviews, "evan", HEAD)).toBe(false);
});
it("not pending if maintainer has at least one review on head (even if also reviewed older commits)", () => {
const reviews = [
{ user_login: "alice", commit_id: HEAD },
{ user_login: "evan", commit_id: HEAD },
];
expect(isReviewPendingFromReviews(reviews, "evan", HEAD)).toBe(false);
});
});