238 lines
14 KiB
JavaScript
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)]));
|
|
})();
|