Files
multica/packages/core/github/pull-request-status.test.ts
e8a16d334f MUL-7753: one verdict per PR in the issue sidebar (#8884)
* feat(core): derive one verdict per pull request (MUL-7753)

Fold CI status and mergeability into a single prioritized verdict for the
issue sidebar, keeping a conflict alongside a failure so neither is lost.
Add helpers to drop the issue's own key from a PR title and to shorten
diff counts for the narrow sidebar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): lead each sidebar PR row with one verdict (MUL-7753)

Each PR now reads title, then repo#number with the diff size and a verdict
pill aligned right, so several PRs scan as a column. Failed PRs list what
failed underneath, one line per check. Owner, author, the full title and
exact counts move to the row tooltip; the section header shows a count.
Drop the Open / Unstable / Has hooks labels and the failed-names sentence.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 18:02:50 +08:00

283 lines
10 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
deriveChecksStatus,
deriveMergeStatus,
derivePullRequestVerdict,
formatPullRequestDiffCount,
shouldShowPullRequestStats,
stripIssueKeyFromTitle,
} from "./pull-request-status";
describe("deriveChecksStatus", () => {
it("maps a `failure` rollup to failed and carries counts + names", () => {
expect(
deriveChecksStatus({
checks_rollup: "failure",
checks_total: 7,
checks_failed: 2,
failed_check_names: ["backend", "e2e"],
}),
).toEqual({ kind: "failed", failed: 2, total: 7, names: ["backend", "e2e"] });
});
it("maps an `error` rollup to failed", () => {
expect(deriveChecksStatus({ checks_rollup: "error", checks_total: 3 }).kind).toBe("failed");
});
it("treats any failed count as failed even when the rollup is absent", () => {
// Failure is trusted from the count so a known failure surfaces even if the
// rollup verdict lags.
expect(deriveChecksStatus({ checks_failed: 1 }).kind).toBe("failed");
});
it("failure beats pending and passed", () => {
expect(
deriveChecksStatus({
checks_rollup: "failure",
checks_failed: 1,
checks_running: 3,
checks_passed: 5,
}).kind,
).toBe("failed");
});
it("maps `pending` / `expected` rollups to pending with running count", () => {
expect(
deriveChecksStatus({
checks_rollup: "pending",
checks_total: 7,
checks_passed: 5,
checks_running: 2,
}),
).toEqual({ kind: "pending", passed: 5, total: 7, running: 2 });
expect(deriveChecksStatus({ checks_rollup: "expected" }).kind).toBe("pending");
});
it("maps a `success` rollup to passed", () => {
expect(deriveChecksStatus({ checks_rollup: "success", checks_total: 7 })).toEqual({
kind: "passed",
total: 7,
});
});
it("renders `none` only for a current snapshot whose rollup is absent", () => {
expect(deriveChecksStatus({ snapshot_available: true }).kind).toBe("none");
expect(
deriveChecksStatus({
snapshot_available: true,
checks_rollup: null,
checks_passed: 5,
checks_total: 5,
}).kind,
).toBe("none");
});
it("hides CI when the API snapshot is disabled or has not landed", () => {
expect(deriveChecksStatus({}).kind).toBe("unavailable");
expect(
deriveChecksStatus({
snapshot_available: false,
checks_rollup: "success",
checks_conclusion: "passed",
checks_total: 5,
}).kind,
).toBe("unavailable");
});
it("preserves legacy provider passed, pending, and failed conclusions", () => {
expect(
deriveChecksStatus({ checks_conclusion: "passed", checks_total: 3 }),
).toEqual({ kind: "passed", total: 3 });
expect(
deriveChecksStatus({
checks_conclusion: "pending",
checks_total: 3,
checks_passed: 2,
checks_pending: 1,
}),
).toEqual({ kind: "pending", passed: 2, total: 3, running: 1 });
expect(
deriveChecksStatus({
checks_conclusion: "failed",
checks_total: 3,
checks_failed: 1,
}).kind,
).toBe("failed");
});
});
describe("deriveMergeStatus", () => {
it("maps `conflicting` to conflicting", () => {
expect(deriveMergeStatus({ mergeable: "conflicting" }).kind).toBe("conflicting");
});
it("folds a `dirty` merge state into conflicting", () => {
expect(deriveMergeStatus({ merge_state_status: "dirty" }).kind).toBe("conflicting");
});
it("asserts ready ONLY from a `clean` merge state", () => {
expect(deriveMergeStatus({ merge_state_status: "clean" }).kind).toBe("ready");
});
it("never infers ready from `mergeable === mergeable` alone", () => {
// "No conflict" is not "ready" — required checks / branch protection live in
// merge_state_status, so mergeable without a clean state renders nothing.
expect(deriveMergeStatus({ mergeable: "mergeable" }).kind).toBe("none");
expect(deriveMergeStatus({ mergeable: "mergeable", merge_state_status: null }).kind).toBe("none");
});
it("surfaces blocked / behind / unstable / has_hooks faithfully", () => {
expect(deriveMergeStatus({ merge_state_status: "blocked" }).kind).toBe("blocked");
expect(deriveMergeStatus({ merge_state_status: "behind" }).kind).toBe("behind");
expect(deriveMergeStatus({ merge_state_status: "unstable" }).kind).toBe("unstable");
expect(deriveMergeStatus({ merge_state_status: "has_hooks" }).kind).toBe("has_hooks");
});
it("renders nothing when GitHub has not decided", () => {
// unknown / null shows neither conflict nor ready.
expect(deriveMergeStatus({}).kind).toBe("none");
expect(deriveMergeStatus({ mergeable: "unknown" }).kind).toBe("none");
expect(deriveMergeStatus({ mergeable: null, merge_state_status: "unknown" }).kind).toBe("none");
expect(deriveMergeStatus({ merge_state_status: "draft" }).kind).toBe("none");
});
it("renders nothing when the API snapshot feature is unavailable", () => {
expect(
deriveMergeStatus({
snapshot_available: false,
mergeable: "conflicting",
merge_state_status: "dirty",
}).kind,
).toBe("none");
});
it("conflict wins over an otherwise decisive merge state", () => {
expect(
deriveMergeStatus({ mergeable: "conflicting", merge_state_status: "blocked" }).kind,
).toBe("conflicting");
});
});
describe("shouldShowPullRequestStats", () => {
it("hides when every field is 0 or missing (legacy backend)", () => {
expect(shouldShowPullRequestStats({})).toBe(false);
expect(shouldShowPullRequestStats({ additions: 0, deletions: 0, changed_files: 0 })).toBe(false);
});
it("shows when at least one number is non-zero", () => {
expect(shouldShowPullRequestStats({ additions: 1 })).toBe(true);
expect(shouldShowPullRequestStats({ deletions: 1 })).toBe(true);
expect(shouldShowPullRequestStats({ changed_files: 1 })).toBe(true);
expect(shouldShowPullRequestStats({ additions: 437, deletions: 6, changed_files: 6 })).toBe(true);
});
});
describe("derivePullRequestVerdict", () => {
const open = { state: "open" as const, snapshot_available: true };
it("ends at merged / closed whatever the snapshot says", () => {
const failing = { checks_rollup: "failure" as const, checks_failed: 2, mergeable: "conflicting" as const };
expect(derivePullRequestVerdict({ ...failing, state: "merged" })).toEqual({ kind: "merged" });
expect(derivePullRequestVerdict({ ...failing, state: "closed" })).toEqual({ kind: "closed" });
});
it("leads with a failure and keeps the names and a conflict alongside it", () => {
expect(
derivePullRequestVerdict({
...open,
checks_rollup: "failure",
checks_total: 19,
checks_failed: 2,
failed_check_names: ["frontend", "backend"],
mergeable: "conflicting",
}),
).toEqual({ kind: "failed", failed: 2, total: 19, names: ["frontend", "backend"], conflicting: true });
});
it("does not let `unstable` add anything to a failure", () => {
expect(
derivePullRequestVerdict({ ...open, checks_rollup: "failure", checks_failed: 1, merge_state_status: "unstable" }),
).toMatchObject({ kind: "failed", conflicting: false });
});
it("puts a conflict ahead of running checks", () => {
expect(
derivePullRequestVerdict({ ...open, checks_rollup: "pending", checks_running: 3, mergeable: "conflicting" }).kind,
).toBe("conflicting");
});
it("reports running checks with their progress", () => {
expect(
derivePullRequestVerdict({ ...open, checks_rollup: "pending", checks_total: 19, checks_passed: 11, checks_running: 8 }),
).toEqual({ kind: "running", passed: 11, total: 19, running: 8 });
});
it.each([
["behind", "behind"],
["blocked", "blocked"],
["clean", "ready"],
["has_hooks", "ready"],
] as const)("maps passing checks with merge state %s to %s", (merge_state_status, kind) => {
expect(
derivePullRequestVerdict({ ...open, checks_rollup: "success", checks_total: 3, merge_state_status }).kind,
).toBe(kind);
});
it("never calls a draft ready, but still surfaces its problems", () => {
const draft = { ...open, state: "draft" as const };
expect(derivePullRequestVerdict({ ...draft, checks_rollup: "success", merge_state_status: "clean" }).kind).toBe("draft");
expect(derivePullRequestVerdict({ ...draft, checks_rollup: "failure", checks_failed: 1 }).kind).toBe("failed");
});
it("falls back to the checks verdict when GitHub has not decided mergeability", () => {
expect(derivePullRequestVerdict({ ...open, checks_rollup: "success", checks_total: 4 })).toEqual({ kind: "passed", total: 4 });
expect(derivePullRequestVerdict({ ...open, checks_rollup: "success", merge_state_status: "unstable" }).kind).toBe("passed");
expect(derivePullRequestVerdict({ ...open, checks_rollup: null }).kind).toBe("no_checks");
});
it("says nothing when there is no current snapshot", () => {
expect(
derivePullRequestVerdict({ state: "open", snapshot_available: false, checks_rollup: "failure", mergeable: "conflicting" }),
).toEqual({ kind: "unknown" });
expect(derivePullRequestVerdict({ state: "open" })).toEqual({ kind: "unknown" });
});
});
describe("stripIssueKeyFromTitle", () => {
it.each([
["MUL-7732: regroup settings", "regroup settings"],
["MUL-7732 regroup settings", "regroup settings"],
["mul-7732: regroup settings", "regroup settings"],
["fix(chat): keep tool rows (MUL-7732)", "fix(chat): keep tool rows"],
])("strips the issue's own key from %j", (title, expected) => {
expect(stripIssueKeyFromTitle(title, "MUL-7732")).toBe(expected);
});
it.each([
["MUL-77321: another issue"],
["MUL-7700: another issue"],
["regroup MUL-7732 settings"],
["MUL-7732"],
])("leaves %j as written", (title) => {
expect(stripIssueKeyFromTitle(title, "MUL-7732")).toBe(title);
});
it("leaves the title alone without an identifier", () => {
expect(stripIssueKeyFromTitle("MUL-1: x", "")).toBe("MUL-1: x");
});
});
describe("formatPullRequestDiffCount", () => {
it.each([
[0, "0"],
[999, "999"],
[1000, "1k"],
[6743, "6.7k"],
[4557, "4.6k"],
[99_960, "100k"],
[123_456, "123k"],
])("formats %d as %s", (count, expected) => {
expect(formatPullRequestDiffCount(count)).toBe(expected);
});
});