Files
2026-07-13 13:12:00 +08:00

238 lines
14 KiB
JavaScript

// Offline unit test for review-sla.js -- exercises the pure decision helpers and
// one end-to-end orchestration of each path against a mocked GitHub client. No
// network. cwd must be the repo root (the orchestrator reads the real
// .github/MAINTAINER; ownership is pinned to a frozen fixture via
// REVIEWER_AREAS_FILE so the test doesn't churn when .github/areas.json changes).
const path = require("path");
const os = require("os");
const fs = require("fs");
const script = require(path.resolve(".github/workflows/review-sla.js"));
// Frozen area fixture: stable owners the orchestration assertions can pin to.
const FIXTURE = {
areas: [
{ key: "inner", label: "comp:harnesses", paths: ["omnigent/inner/"], owners: ["ownerA", "ownerB", "ownerC"] },
{ key: "web", label: "comp:web-ui", paths: ["web/"], owners: ["webX", "webY"] },
],
};
const FIXTURE_PATH = path.join(os.tmpdir(), "review-sla-areas.fixture.json");
fs.writeFileSync(FIXTURE_PATH, JSON.stringify(FIXTURE));
process.env.REVIEWER_AREAS_FILE = FIXTURE_PATH;
function assert(name, cond, detail) {
console.log(`${cond ? "PASS" : "FAIL"} ${name}${detail ? " -- " + detail : ""}`);
if (!cond) process.exitCode = 1;
}
const daysAgoIso = (n) => new Date(Date.now() - n * 86400000).toISOString();
// Mocked GitHub client. `canned` maps a list-endpoint tag -> the array it returns
// through github.paginate; writes are recorded in `sink`. `failRequestReviewers`
// makes pulls.requestReviewers throw, to exercise the partial-failure path.
function mkGithub(canned, sink, opts = {}) {
const list = (tag) => { const f = async () => {}; f._tag = tag; return f; };
return {
paginate: async (fn) => canned[fn._tag] || [],
rest: {
pulls: {
list: list("openPRs"),
listReviews: list("reviews"),
listReviewComments: list("reviewComments"),
listFiles: list("files"),
requestReviewers: async (a) => {
if (opts.failRequestReviewers) throw new Error("HTTP 422: reviewer is not a collaborator");
sink.requested.push(...a.reviewers);
},
},
issues: {
listForRepo: list("openIssues"),
listEventsForTimeline: list("timeline"),
listComments: list("comments"),
createComment: async (a) => sink.comments.push(a),
addAssignees: async (a) => sink.assigned.push(...a.assignees),
addLabels: async (a) => sink.labels.push(...a.labels),
},
},
};
}
async function runOrch(canned, opts) {
const sink = { comments: [], requested: [], assigned: [], labels: [], warnings: [] };
const core = { info: () => {}, warning: (m) => sink.warnings.push(m) };
const context = { repo: { owner: "omnigent-ai", repo: "omnigent" } };
await script({ github: mkGithub(canned, sink, opts), context, core });
return sink;
}
(async () => {
// ---- workingDaysBetween (2026-01-05 is a Monday, 01-12 the next Monday) ----
const wdb = script.workingDaysBetween;
assert("same day -> 0", wdb("2026-01-05", "2026-01-05") === 0);
assert("Mon -> next Mon (7 cal days) -> 5 working days", wdb("2026-01-05", "2026-01-12") === 5, String(wdb("2026-01-05", "2026-01-12")));
assert("Fri -> Mon spans a weekend -> 1", wdb("2026-01-09", "2026-01-12") === 1, String(wdb("2026-01-09", "2026-01-12")));
assert("Sat -> Sun -> 0", wdb("2026-01-10", "2026-01-11") === 0);
// ---- latestByUser ----
const tl = [
{ event: "review_requested", requested_reviewer: { login: "Alice" }, created_at: "2026-01-01T00:00:00Z" },
{ event: "review_requested", requested_reviewer: { login: "Alice" }, created_at: "2026-01-03T00:00:00Z" },
{ event: "assigned", assignee: { login: "Bob" }, created_at: "2026-01-02T00:00:00Z" },
];
const rq = script.latestByUser(tl, "review_requested", (e) => e.requested_reviewer && e.requested_reviewer.login);
assert("latestByUser keeps the newer event", rq.alice === "2026-01-03T00:00:00Z", JSON.stringify(rq));
assert("latestByUser ignores other event types", !("bob" in rq));
// ---- repliedSince ----
const since = "2026-01-01T00:00:00Z";
assert("comment after -> replied",
script.repliedSince("alice", since, [{ user: { login: "Alice" }, created_at: "2026-01-02T00:00:00Z" }], [], []) === true);
assert("comment before -> not replied",
script.repliedSince("alice", since, [{ user: { login: "Alice" }, created_at: "2025-12-31T00:00:00Z" }], [], []) === false);
assert("review after -> replied",
script.repliedSince("alice", since, [], [{ user: { login: "alice" }, submitted_at: "2026-01-05T00:00:00Z" }], []) === true);
assert("someone else's comment -> not replied",
script.repliedSince("alice", since, [{ user: { login: "Bob" }, created_at: "2026-01-09T00:00:00Z" }], [], []) === false);
// ---- alreadyNudged (marker fallback) ----
assert("alreadyNudged: marker present -> true", script.alreadyNudged([{ body: "hi " + script.MARKER }]) === true);
assert("alreadyNudged: no marker -> false", script.alreadyNudged([{ body: "just a normal comment" }]) === false);
// ---- breachedTargets ----
const now = new Date();
const b1 = script.breachedTargets({
targets: ["Alice"], clockStartByUser: { alice: daysAgoIso(14) }, openedAt: daysAgoIso(30), now,
comments: [], reviews: [], reviewComments: [],
});
assert("stale + silent -> breached", JSON.stringify(b1) === JSON.stringify(["Alice"]), JSON.stringify(b1));
const b2 = script.breachedTargets({
targets: ["Alice"], clockStartByUser: { alice: daysAgoIso(1) }, openedAt: daysAgoIso(1), now,
comments: [], reviews: [], reviewComments: [],
});
assert("within SLA -> not breached", b2.length === 0, JSON.stringify(b2));
const b3 = script.breachedTargets({
targets: ["Alice"], clockStartByUser: { alice: daysAgoIso(14) }, openedAt: daysAgoIso(30), now,
comments: [{ user: { login: "Alice" }, created_at: daysAgoIso(1) }], reviews: [], reviewComments: [],
});
assert("stale but replied -> not breached", b3.length === 0, JSON.stringify(b3));
// ---- parseAreas ----
const { rules, pool, labelOwners } = script.parseAreas(JSON.stringify(FIXTURE));
assert("parseAreas: rules preserve prefixes", rules.some((r) => r.prefix === "omnigent/inner/") && rules.some((r) => r.prefix === "web/"), JSON.stringify(rules));
assert("parseAreas: pool unions all owners", ["ownera", "ownerb", "ownerc", "webx", "weby"].every((o) => pool.has(o)), JSON.stringify([...pool.keys()]));
assert("parseAreas: labelOwners maps comp:* -> owners", [...(labelOwners.get("comp:web-ui") || [])].sort().join(",") === "webX,webY", JSON.stringify([...(labelOwners.get("comp:web-ui") || [])]));
// ---- pickSecondReviewer ----
const srMembers = script.pickSecondReviewer({
files: ["omnigent/inner/foo.py"], rules, pool, load: new Map(),
exclude: new Set(["ownera"]),
});
assert("second reviewer is an inner owner, excluding those on the PR",
["ownerb", "ownerc"].includes((srMembers || "").toLowerCase()), String(srMembers));
const srLoad = script.pickSecondReviewer({
files: ["omnigent/inner/foo.py"], rules, pool,
load: new Map([["ownera", 5], ["ownerb", 5], ["ownerc", 0]]),
exclude: new Set(),
});
assert("lowest-load owner wins the tie-break", (srLoad || "").toLowerCase() === "ownerc", String(srLoad));
const srFallback = script.pickSecondReviewer({
files: ["README.md"], rules, pool, load: new Map(), exclude: new Set(),
});
assert("unowned path -> falls back to the full pool", pool.has((srFallback || "").toLowerCase()), String(srFallback));
// ---- pickSecondAssignee ----
const saMatch = script.pickSecondAssignee({
labels: ["comp:web-ui"], labelOwners, pool, load: new Map(), exclude: new Set(["webx"]),
});
assert("second assignee comes from the label's owners, excluding the current one",
(saMatch || "").toLowerCase() === "weby", String(saMatch));
const saFallback = script.pickSecondAssignee({
labels: [], labelOwners, pool, load: new Map(), exclude: new Set(),
});
assert("no comp label -> falls back to the full pool", pool.has((saFallback || "").toLowerCase()), String(saFallback));
// ---- orchestration: a stale, silent PR gets nudged + a 2nd reviewer + label --
const stalePR = {
number: 7, draft: false, labels: [], user: { login: "someexternaldev" },
created_at: daysAgoIso(14), requested_reviewers: [{ login: "dhruv0811" }], assignees: [{ login: "dhruv0811" }],
};
let s = await runOrch({
openPRs: [stalePR], openIssues: [], timeline: [], comments: [], reviews: [], reviewComments: [],
files: [{ filename: "omnigent/inner/foo.py" }],
});
assert("stale PR: one reminder comment posted", s.comments.length === 1 && s.comments[0].issue_number === 7, JSON.stringify(s.comments));
assert("stale PR: comment re-pings the assigned reviewer", /@dhruv0811/.test(s.comments[0].body), s.comments[0] && s.comments[0].body);
assert("stale PR: a second reviewer is requested from the area owners",
s.requested.length === 1 && ["ownera", "ownerb", "ownerc"].includes(s.requested[0].toLowerCase()), JSON.stringify(s.requested));
assert("stale PR: second reviewer mirrored as assignee", JSON.stringify(s.assigned) === JSON.stringify(s.requested), JSON.stringify(s.assigned));
assert("stale PR: comment names exactly the reviewer that was added",
new RegExp(`Adding @${s.requested[0]} as a second reviewer`).test(s.comments[0].body), s.comments[0] && s.comments[0].body);
assert("stale PR: comment carries the idempotency marker", s.comments[0].body.includes(script.MARKER), s.comments[0] && s.comments[0].body);
assert("stale PR: labelled once", JSON.stringify(s.labels) === JSON.stringify([script.LABEL]), JSON.stringify(s.labels));
// ---- orchestration: partial failure -- requestReviewers throws --
// add-first ordering means the comment must NOT claim a 2nd reviewer that failed
// to attach, yet the item is still labelled so it won't be re-nudged tomorrow.
s = await runOrch({
openPRs: [stalePR], openIssues: [], timeline: [], comments: [], reviews: [], reviewComments: [],
files: [{ filename: "omnigent/inner/foo.py" }],
}, { failRequestReviewers: true });
assert("partial failure: reminder comment still posted", s.comments.length === 1, JSON.stringify(s.comments));
assert("partial failure: comment does NOT over-claim a second reviewer", !/second reviewer/.test(s.comments[0].body), s.comments[0] && s.comments[0].body);
assert("partial failure: no reviewer was actually requested", s.requested.length === 0, JSON.stringify(s.requested));
assert("partial failure: still labelled (won't re-nudge next run)", JSON.stringify(s.labels) === JSON.stringify([script.LABEL]), JSON.stringify(s.labels));
assert("partial failure: the reviewer-add error is warned, not fatal", s.warnings.some((w) => /could not add second reviewer/.test(w)), JSON.stringify(s.warnings));
// ---- orchestration: marker fallback -- prior nudge exists but the label didn't --
s = await runOrch({
openPRs: [stalePR], openIssues: [], timeline: [], reviews: [], reviewComments: [],
files: [{ filename: "omnigent/inner/foo.py" }],
comments: [{ user: { login: "omnigent-ci" }, body: script.MARKER + "\nearlier nudge", created_at: daysAgoIso(2) }],
});
assert("marker fallback: an already-nudged PR (marker present, no label) is skipped",
s.comments.length === 0 && s.labels.length === 0, JSON.stringify(s));
// ---- orchestration: already-labelled PR is left alone (one-shot) ----
s = await runOrch({ openPRs: [{ ...stalePR, labels: [{ name: script.LABEL }] }], openIssues: [], files: [] });
assert("already-escalated PR is skipped", s.comments.length === 0 && s.labels.length === 0, JSON.stringify(s));
// ---- orchestration: a fresh PR (within SLA) is left alone ----
s = await runOrch({ openPRs: [{ ...stalePR, created_at: daysAgoIso(1) }], openIssues: [], timeline: [], files: [] });
assert("fresh PR is not escalated", s.comments.length === 0, JSON.stringify(s));
// ---- orchestration: a PR whose reviewer already commented is left alone ----
s = await runOrch({
openPRs: [stalePR], openIssues: [], timeline: [], reviews: [], reviewComments: [], files: [],
comments: [{ user: { login: "dhruv0811" }, created_at: daysAgoIso(1) }],
});
assert("PR with a recent reply is not escalated", s.comments.length === 0, JSON.stringify(s));
// ---- orchestration: a stale, silent issue gets nudged + a 2nd assignee + label --
const staleIssue = {
number: 9, labels: [{ name: "comp:web-ui" }], created_at: daysAgoIso(14), assignees: [{ login: "hzub" }],
};
s = await runOrch({ openPRs: [], openIssues: [staleIssue], timeline: [], comments: [] });
assert("stale issue: one reminder comment posted", s.comments.length === 1 && s.comments[0].issue_number === 9, JSON.stringify(s.comments));
assert("stale issue: re-pings the assignee", /@hzub/.test(s.comments[0].body), s.comments[0] && s.comments[0].body);
assert("stale issue: a second assignee from the label's owners", ["webx", "weby"].includes((s.assigned[0] || "").toLowerCase()), JSON.stringify(s.assigned));
assert("stale issue: labelled once", JSON.stringify(s.labels) === JSON.stringify([script.LABEL]), JSON.stringify(s.labels));
// ---- orchestration: a real PR object (listForRepo) is not double-swept as an issue --
s = await runOrch({ openPRs: [], openIssues: [{ ...staleIssue, pull_request: {} }], timeline: [], comments: [] });
assert("PR returned by listForRepo is skipped in the issue sweep", s.comments.length === 0, JSON.stringify(s));
// ---- orchestration: per-run cap + in-sweep load spread ----
// Feed more stale PRs than the cap. Expect exactly MAX escalations, and the
// second reviewer rotates across all 3 inner owners rather than dogpiling the
// one lowest-load maintainer (regression for the live-data concentration bug).
const MAX = script.MAX_ESCALATIONS_PER_RUN;
const manyStale = Array.from({ length: MAX + 5 }, (_, i) => ({ ...stalePR, number: 3000 + i }));
s = await runOrch({
openPRs: manyStale, openIssues: [], timeline: [], comments: [], reviews: [], reviewComments: [],
files: [{ filename: "omnigent/inner/foo.py" }],
});
assert("cap: escalations stop at MAX_ESCALATIONS_PER_RUN", s.comments.length === MAX, `${s.comments.length} vs ${MAX}`);
assert("cap: labels capped to match", s.labels.length === MAX, String(s.labels.length));
assert("load spread: second reviewer rotates across all 3 inner owners (not dogpiled on one)",
new Set(s.requested.map((u) => u.toLowerCase())).size === 3, JSON.stringify([...new Set(s.requested)]));
})();