// **A merge check's verdict holds for the trunk commit it judged** (novox/hq ADR 0323, issue 341): both statuses name // the head and the trunk commit; a trunk's move puts the merge check of every open pull request into that trunk — and // of none into another — back to pending with the reason, and runs nothing; a verdict that lands judged against a trunk // that has moved since is said out of date. import { test } from "node:test"; import assert from "node:assert/strict"; import { checkOnItsWay, lateVerdictComment, outOfDateStatuses, outOfDateWords, trunkNamed, verdictAtTrunk, weighTrunks } from "../dist/trunk.js"; import { GateWriter } from "../dist/gate.js"; import { statusesFor, commentFor } from "../dist/pulls.js"; const HEAD = "dfb1415aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const TRUNK = "a2c2c35bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; const MOVED = "d28ca95ccccccccccccccccccccccccccccccccc"; test("both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length", () => { const c = { owner: "novox", repo: "mesh-host", number: 66, commit: HEAD, verdict: "pass", summary: "x", id: "build-1", trunk: TRUNK.slice(0, 12), beside: { "mesh-catalog": "0123456789ab", "mesh-lab": "ba9876543210" }, gate: { verdict: "pass", summary: "4 of 4 compose ".repeat(20) }, "repo-check": { verdict: "fail", summary: "its merge-check.sh failed: internal/store/store.go:145:2: Declared redeclared" } }; const [gate, repo] = statusesFor(c); for (const s of [gate, repo]) { assert.ok(s.description.startsWith("head dfb1415aaaaa merged into trunk a2c2c35bbbbb — "), s.description); assert.ok(s.description.length <= 140, `${s.description.length} characters`); assert.equal(trunkNamed(s.description), "a2c2c35bbbbb"); } assert.equal(gate.state, "success"); assert.equal(repo.state, "failure"); const comment = commentFor(c)!; assert.match(comment, /Judged: head `dfb1415aaaaa` merged into trunk `a2c2c35bbbbb`; read beside it: mesh-catalog `0123456789ab`, mesh-lab `ba9876543210`/); // A verdict of a build seat from before the rule names no trunk, and is said as before. const [old] = statusesFor({ ...c, trunk: undefined, gate: { verdict: "pass", summary: "every machine composes" } }); assert.equal(old.description, "pass: every machine composes"); assert.equal(trunkNamed(old.description), undefined); }); test("a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way", () => { const verdict = (trunk: string, state = "success") => ({ "mesh/merge-gate": { state, description: `head ${HEAD.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: x` }, "mesh/repo-check": { state, description: `head ${HEAD.slice(0, 12)} merged into trunk ${trunk.slice(0, 12)} — pass: y` } }); const put = outOfDateStatuses(verdict(TRUNK), MOVED); assert.deepEqual(put.map((s) => [s.context, s.state]), [["mesh/merge-gate", "pending"], ["mesh/repo-check", "pending"]]); assert.equal(put[0].description, "out of date: judged against trunk a2c2c35bbbbb, the trunk is now d28ca95ccccc; " + "judged again on a recheck or when it is merged"); assert.equal(put[0].description, outOfDateWords(TRUNK, MOVED)); assert.equal(trunkNamed(put[0].description), "a2c2c35bbbbb", "an out-of-date status still names what it judged"); // A failure is out of date too: it judged another merge. assert.equal(outOfDateStatuses(verdict(TRUNK, "failure"), MOVED).length, 2); // Already judged against the trunk's head; a check on its way or already out of date; no check yet: left alone. assert.deepEqual(outOfDateStatuses(verdict(MOVED), MOVED), []); assert.deepEqual(outOfDateStatuses({ "mesh/merge-gate": { state: "pending", description: "building on ace since 14:02" } }, MOVED), []); assert.deepEqual(outOfDateStatuses({ "mesh/merge-gate": { state: "pending", description: put[0].description } }, MOVED), []); assert.deepEqual(outOfDateStatuses({}, MOVED), []); // A head with no repository check of its own gets no pending one a verdict would never set. assert.deepEqual(outOfDateStatuses({ "mesh/merge-gate": verdict(TRUNK)["mesh/merge-gate"] }, MOVED).map((s) => s.context), ["mesh/merge-gate"]); // A verdict naming no trunk, of a build seat from before the rule, judged an earlier trunk. assert.match(outOfDateStatuses({ "mesh/merge-gate": { state: "success", description: "pass: x" } }, MOVED)[0].description, /judged against a trunk it does not name, the trunk is now d28ca95ccccc/); assert.ok(checkOnItsWay({ state: "pending", description: "waiting in the build queue since 14:00" })); assert.ok(checkOnItsWay({ state: "pending", description: "checking again: a person asked" })); assert.ok(!checkOnItsWay({ state: "pending", description: put[0].description })); assert.deepEqual(verdictAtTrunk(verdict(MOVED), MOVED), { current: true, onItsWay: false, judged: "d28ca95ccccc" }); assert.equal(verdictAtTrunk(verdict(TRUNK), MOVED).current, false); }); /** A forge holding open pull requests into two branches, each head's statuses, and what was written. */ function aForge(pulls: { number: number; base: string; head: string }[], heads: Record) { const statuses = new Map>(); const writes: string[] = []; const verdict = (trunk: string) => ({ "mesh/merge-gate": { state: "success", description: `head x merged into trunk ${trunk.slice(0, 12)} — pass` }, "mesh/repo-check": { state: "success", description: `head x merged into trunk ${trunk.slice(0, 12)} — pass` } }); for (const p of pulls) statuses.set(p.head, verdict(heads[p.base])); const forge = { listOpenPulls: async () => pulls.map((p) => ({ number: p.number, title: "t", state: "open", merged: false, base: p.base, head: "feat", head_sha: p.head, html_url: `http://forge/pulls/${p.number}` })), branchHead: async (_o: string, _r: string, branch: string) => heads[branch], setCommitStatus: async (_o: string, _r: string, sha: string, s: { context: string; state: string; description: string }) => { writes.push(`${sha} ${s.context} ${s.state}`); statuses.set(sha, { ...statuses.get(sha), [s.context]: { state: s.state, description: s.description } }); }, gateOf: async () => undefined, gateHistory: async () => [], headChecks: async (_o: string, _r: string, sha: string) => statuses.get(sha) ?? {}, }; return { forge, writes, statuses }; } test("a push to a trunk puts every open pull request into it out of date — twenty-five of them — and none into another", async () => { const heads: Record = { main: TRUNK, release: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }; const pulls = Array.from({ length: 25 }, (_, i) => ({ number: i + 1, base: "main", head: `${String(i).padStart(2, "0")}`.repeat(20) })); pulls.push({ number: 99, base: "release", head: "9".repeat(40) }); const { forge, writes, statuses } = aForge(pulls, heads); const gates = new GateWriter(forge); const trunks: Record = {}; // First sight: each trunk is taken as it is, and nothing is put out of date. assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-host"), true); assert.deepEqual(writes, []); assert.equal(trunks["novox/mesh-host:main"], TRUNK); // Nothing moved: nothing read past the trunks, nothing written. assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-host"), false); heads.main = MOVED; assert.equal(await weighTrunks(forge, gates, trunks, "novox", "mesh-host"), true); assert.equal(writes.length, 50, "both statuses of each of the 25 pull requests into main"); for (const p of pulls.filter((p) => p.base === "main")) { const s = statuses.get(p.head)!; assert.equal(s["mesh/merge-gate"].state, "pending"); assert.match(s["mesh/merge-gate"].description!, /judged against trunk a2c2c35bbbbb, the trunk is now d28ca95ccccc/); assert.equal(s["mesh/repo-check"].state, "pending"); } assert.equal(statuses.get("9".repeat(40))!["mesh/merge-gate"].state, "success", "a pull request into another branch is left"); assert.equal(trunks["novox/mesh-host:main"], MOVED); // Weighed again, nothing more: the move was taken once, and the out-of-date statuses are left as they are. writes.length = 0; heads.main = "f".repeat(40); await weighTrunks(forge, gates, trunks, "novox", "mesh-host"); assert.deepEqual(writes, [], "a check already out of date is not written again"); }); test("a trunk that cannot be read is weighed again on the next pass, never recorded half", async () => { const heads: Record = { main: TRUNK }; const { forge } = aForge([{ number: 1, base: "main", head: "1".repeat(40) }], heads); const trunks: Record = { "novox/mesh-host:main": TRUNK }; heads.main = MOVED; const failing = { ...forge, setCommitStatus: async () => { throw new Error("the forge is away"); } }; assert.equal(await weighTrunks(failing, new GateWriter(failing), trunks, "novox", "mesh-host"), false); assert.equal(trunks["novox/mesh-host:main"], TRUNK, "not recorded: the next pass weighs it again"); }); test("the trunk's move takes its turn after a verdict in flight, and weighs what the forge holds then", async () => { const heads: Record = { main: TRUNK }; const { forge, statuses } = aForge([{ number: 1, base: "main", head: HEAD }], heads); const gates = new GateWriter(forge); // A verdict judged against the trunk's new head, in flight when the move is weighed: it lands first, and is left. let release!: () => void; const held = new Promise((r) => (release = r)); const verdictLanded = gates.verdict("novox", "mesh-host", HEAD, async () => { await held; statuses.set(HEAD, { "mesh/merge-gate": { state: "success", description: `head x merged into trunk ${MOVED.slice(0, 12)} — pass` } }); }); const moved = gates.outOfDate("novox", "mesh-host", HEAD, (checks) => outOfDateStatuses(checks, MOVED)); release(); await verdictLanded; assert.deepEqual(await moved, [], "a verdict judged against the trunk's head is not put out of date"); assert.equal(statuses.get(HEAD)!["mesh/merge-gate"].state, "success"); }); test("a verdict judged against a trunk newer than the pass's first read is not put out of date", async () => { const NEWER = "ffffffffffffffffffffffffffffffffffffffff"; const heads: Record = { main: TRUNK }; const { forge, statuses } = aForge([{ number: 1, base: "main", head: HEAD }], heads); const trunks: Record = { "novox/mesh-host:main": TRUNK }; // The pass reads main at MOVED; by the head's turn the trunk moved again, and the head's check answered for it. let reads = 0; const moving = { ...forge, branchHead: async () => (reads++ === 0 ? MOVED : NEWER) }; statuses.set(HEAD, { "mesh/merge-gate": { state: "success", description: `head x merged into trunk ${NEWER.slice(0, 12)} — pass` } }); await weighTrunks(moving, new GateWriter(moving), trunks, "novox", "mesh-host"); assert.equal(statuses.get(HEAD)!["mesh/merge-gate"].state, "success", "a fresh verdict was put out of date"); }); test("a verdict that landed out of date keeps its account on the pull request", () => { const words = outOfDateWords(TRUNK, MOVED); const own = lateVerdictComment({ commit: HEAD, trunk: TRUNK.slice(0, 12), verdict: "fail" }, "**Merge check** at `dfb1415a`\n\nthe report", words); assert.match(own, /the report/); assert.match(own, /\*\*Out of date when it landed\*\*: out of date: judged against trunk a2c2c35bbbbb/); const bare = lateVerdictComment({ commit: HEAD, trunk: TRUNK.slice(0, 12), verdict: "pass" }, null, words); assert.match(bare, /head dfb1415aaaaa merged into trunk a2c2c35bbbbb, pass/); });