// **A delivery group's member is judged beside its siblings, and a sibling's move puts that verdict out of date** // (novox/hq ADR 0382 §4-§6, issue 867): the holder keeps the siblings each verdict names; a sibling's new head, close // unmerged or base move (other than its own merge of the head that was read) sets both statuses of every member whose // verdict names it back to pending, and runs nothing; a member merges only with its group passed and every sibling head // read approved. import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { besideWords, groupRefusal, keepSiblings, namesSiblings, noteVerdict, readSiblings, siblingOutOfDate, siblingsFile, siblingsOf, siblingStatuses, weighSiblings, type SiblingRecord } from "../dist/siblings.js"; import { weighTrunks } from "../dist/trunk.js"; import { commentFor, statusesFor } from "../dist/pulls.js"; const h = (c: string) => c.repeat(40); const MEMBER_HEAD = h("a"), HOST_HEAD = h("b"), HOST_BASE = h("c"), HOST_NEW = h("d"), HOST_MOVED = h("e"), MERGE = h("f"); const sibling = { repository: "novox/mesh-host", number: 114, head: HOST_HEAD, base: "main", base_commit: HOST_BASE }; const verdict = { owner: "novox", repo: "mesh-controller", number: 346, commit: MEMBER_HEAD, verdict: "pass", summary: "x", id: "build-1", trunk: h("1").slice(0, 12), siblings: [sibling], judge_from: "sibling novox/mesh-controller#346 merge 123456789abc", gate: { verdict: "pass", summary: "every machine composes ".repeat(10) }, "repo-check": { verdict: "pass", summary: "ok" } }; test("both statuses name the siblings the verdict read, keeping the verdict word and the start of its reason within 140 characters", () => { // A realistic controller summary for a failure, long enough that the forge clips it. const reason = "2 of 4 machines do not compose: ace: mesh-host.service needs seat node-reads, which no module on ace claims after this change"; const c = { ...verdict, gate: { verdict: "fail", summary: reason }, "repo-check": { verdict: "fail", summary: "its merge-check.sh failed: internal/link/check.go:145:2: undefined" } }; const [gate, repo] = statusesFor(c); for (const s of [gate, repo]) { assert.ok(s.description.startsWith(`head ${MEMBER_HEAD.slice(0, 12)} merged into trunk 111111111111 beside mesh-host#114@bbbbbbbb — fail: `), s.description); assert.ok(s.description.length <= 140, `${s.description.length} characters`); assert.ok(namesSiblings(s.description)); } assert.match(gate.description, /fail: 2 of 4 machines do not compose: ace: mesh-host/, gate.description); assert.match(repo.description, /fail: its merge-check\.sh failed: internal/, repo.description); assert.equal(besideWords([...siblingsOf([sibling]), ...siblingsOf([{ ...sibling, number: 115 }])]), "beside mesh-host#114@bbbbbbbb +1 more"); const comment = commentFor({ ...verdict, gate: { verdict: "fail", summary: "no" } })!; assert.match(comment, /Siblings read beside it: novox\/mesh-host#114 `bbbbbbbbbbbb` merged onto `cccccccccccc` \(main\); judged by sibling novox\/mesh-controller#346 merge/); }); test("a verdict that could not learn its siblings says so on the status and in the comment, and names none", () => { const { siblings, ...unknown } = { ...verdict, siblings_unknown: "mesh-delivery did not answer" }; const reason = "2 of 4 machines do not compose: ace: mesh-host.service needs seat node-reads, which no module claims"; for (const s of statusesFor({ ...unknown, gate: { verdict: "fail", summary: reason } })) { assert.match(s.description, /^head aaaaaaaaaaaa merged into trunk 111111111111 siblings unknown — (fail|pass): /, s.description); assert.ok(s.description.length <= 140); assert.equal(namesSiblings(s.description), false, "it read no siblings, so the merge tool asks for none"); } assert.match(commentFor({ ...unknown, gate: { verdict: "fail", summary: "no" } })!, /Siblings could not be learned: mesh-delivery did not answer/); }); test("a verdict without siblings is said exactly as before (an older controller)", () => { const { siblings, judge_from, ...old } = verdict; const [gate] = statusesFor(old); assert.equal(namesSiblings(gate.description), false); assert.match(gate.description, /^head aaaaaaaaaaaa merged into trunk 111111111111 — pass: /); assert.deepEqual(siblingsOf(undefined), []); assert.deepEqual(siblingsOf([{ repository: "novox/x" }]), [], "a sibling named without its head is none"); }); test("the siblings a verdict names are kept per member, replaced by its next verdict, and removed by one that read none", () => { const file = siblingsFile(mkdtempSync(join(tmpdir(), "gitea-siblings-"))); const record: SiblingRecord = {}; assert.equal(noteVerdict(record, "novox/mesh-controller#346", MEMBER_HEAD, []), false); assert.equal(noteVerdict(record, "novox/mesh-controller#346", MEMBER_HEAD, siblingsOf([sibling])), true); keepSiblings(file, record); assert.deepEqual(readSiblings(file), record, "kept where the holder keeps its state"); assert.equal(noteVerdict(record, "novox/mesh-controller#346", MEMBER_HEAD, []), true); assert.deepEqual(record, {}); assert.deepEqual(readSiblings(null), {}); }); // --- a sibling's move ----------------------------------------------------------------------------------------- interface World { host: { merged: boolean; state: string; head_sha: string; merge_commit_sha?: string }; hostBase: string; parents: string[]; member: { head_sha: string; state: string; merged: boolean }; checks: Record; written: { sha: string; context: string; state: string; description: string }[]; reads: string[]; } const world = (): World => ({ host: { merged: false, state: "open", head_sha: HOST_HEAD }, hostBase: HOST_BASE, parents: [], member: { head_sha: MEMBER_HEAD, state: "open", merged: false }, checks: { "mesh/merge-gate": { state: "success", description: "head aaaaaaaaaaaa merged into trunk 111111111111 beside mesh-host#114@bbbbbbbb — pass: x" }, "mesh/repo-check": { state: "success", description: "pass: y" } }, written: [], reads: [] }); const forgeOf = (w: World) => ({ async getPullRequest(owner: string, repo: string, n: number) { w.reads.push(`${owner}/${repo}#${n}`); return repo === "mesh-host" ? { number: n, title: "", html_url: "", ...w.host } : { number: n, title: "", html_url: "", merged: false, ...w.member }; }, async branchHead(_o: string, _r: string, _b: string) { return w.hostBase; }, async commitParents() { return w.parents; }, }); const gatesOf = (w: World) => ({ async outOfDate(_o: string, _r: string, sha: string, decide: (c: any) => any[] | Promise) { const put = await decide(w.checks); for (const s of put) { w.written.push({ sha, ...s }); w.checks[s.context] = { state: s.state, description: s.description }; } return put; }, }); const recordOf = (): SiblingRecord => ({ "novox/mesh-controller#346": { head: MEMBER_HEAD, siblings: siblingsOf([sibling]) } }); const weigh = (w: World, record: SiblingRecord, repository = "novox/mesh-host", number?: number) => weighSiblings(forgeOf(w) as any, gatesOf(w) as any, record, repository, number); test("a sibling's new head puts both statuses of the member back to pending, with the reason, and runs nothing", async () => { const w = world(); w.host.head_sha = HOST_NEW; await weigh(w, recordOf(), "novox/mesh-host", 114); assert.deepEqual(w.written.map((s) => [s.sha, s.context, s.state]), [[MEMBER_HEAD, "mesh/merge-gate", "pending"], [MEMBER_HEAD, "mesh/repo-check", "pending"]]); assert.equal(w.written[0].description, "out of date: judged beside mesh-host#114 at bbbbbbbbbbbb, it is now at dddddddddddd; judged again on a recheck or when it is merged"); assert.ok(siblingOutOfDate(w.written[0].description)); // Already pending (out of date, or a check on its way): not written again. w.written.length = 0; await weigh(w, recordOf(), "novox/mesh-host", 114); assert.deepEqual(w.written, []); }); test("a sibling closed unmerged puts the member's verdict out of date", async () => { const w = world(); w.host.state = "closed"; await weigh(w, recordOf(), "novox/mesh-host", 114); assert.equal(w.written.length, 2); assert.match(w.written[0].description, /^out of date: judged beside mesh-host#114 was closed unmerged/); }); test("a move of the sibling's base puts it out of date, naming both commits", async () => { const w = world(); w.hostBase = HOST_MOVED; await weigh(w, recordOf()); assert.equal(w.written.length, 2); assert.match(w.written[0].description, /mesh-host#114 merged onto cccccccccccc, its main is now eeeeeeeeeeee/); }); test("the sibling's own merge of the head read onto the base read does not, in either order of the two merges", async () => { // The sibling merges first (a merge commit of the base commit read and the head read). const w = world(); Object.assign(w.host, { merged: true, state: "closed", merge_commit_sha: MERGE }); w.hostBase = MERGE; w.parents = [HOST_BASE, HOST_HEAD]; const record = recordOf(); assert.equal(await weigh(w, record), true, "the record changes: the sibling is marked merged"); assert.deepEqual(w.written, []); assert.equal(record["novox/mesh-controller#346"].siblings[0].merged, true); // A move of that base afterwards is no sibling's move: the sibling is merged. w.hostBase = HOST_MOVED; await weigh(w, record); assert.deepEqual(w.written, []); // The member merges first: its own repository moves, which no member's verdict names as a sibling. const m = world(); m.member.merged = true; await weigh(m, recordOf(), "novox/mesh-controller"); assert.deepEqual(m.written, []); // And the sibling then merges: still not a move. Object.assign(m.host, { merged: true, state: "closed", merge_commit_sha: MERGE }); m.parents = [HOST_BASE, HOST_HEAD]; await weigh(m, recordOf()); assert.deepEqual(m.written, []); // A squash is one parent: the base commit read. const q = world(); Object.assign(q.host, { merged: true, state: "closed", merge_commit_sha: MERGE }); q.parents = [HOST_BASE]; await weigh(q, recordOf()); assert.deepEqual(q.written, []); }); test("a merge of another head, or onto another base, is a move", async () => { const other = world(); Object.assign(other.host, { merged: true, state: "closed", merge_commit_sha: MERGE, head_sha: HOST_NEW }); other.parents = [HOST_BASE, HOST_NEW]; await weigh(other, recordOf()); assert.equal(other.written.length, 2); const onto = world(); Object.assign(onto.host, { merged: true, state: "closed", merge_commit_sha: MERGE }); onto.parents = [HOST_MOVED, HOST_HEAD]; await weigh(onto, recordOf()); assert.equal(onto.written.length, 2); }); test("only the members whose verdict names the sibling are touched, and a member that moved on is dropped", async () => { // Another sibling's move, or another repository's: nothing read, nothing written. const w = world(); w.host.head_sha = HOST_NEW; await weigh(w, recordOf(), "novox/mesh-lab"); await weigh(w, recordOf(), "novox/mesh-host", 999); assert.deepEqual(w.written, []); // A verdict that is none (a gate not yet judged) is left alone. const none = world(); none.host.head_sha = HOST_NEW; none.checks = {}; await weigh(none, recordOf()); assert.deepEqual(none.written, []); // The member has a new head: its old verdict is no verdict; the record is dropped, nothing written to the old head. const moved = world(); moved.host.head_sha = HOST_NEW; moved.member.head_sha = h("9"); const record = recordOf(); assert.equal(await weigh(moved, record), true); assert.deepEqual(moved.written, []); assert.deepEqual(record, {}); // A repository without a repo-check gets no pending one a verdict would never set. assert.deepEqual(siblingStatuses({ "mesh/merge-gate": { state: "success" } }, "x").map((s) => s.context), ["mesh/merge-gate"]); }); test("a trunk that moved tells the siblings' weighing; one first seen does not", async () => { const told: string[] = []; const forge = { listOpenPulls: async () => [{ number: 1, title: "", state: "open", merged: false, head_sha: h("2"), base: "main", html_url: "" }], branchHead: async () => h("3") }; const gates = { outOfDate: async () => [] }; const trunks: Record = {}; await weighTrunks(forge as any, gates as any, trunks, "novox", "mesh-host", async (b) => { told.push(b); }); assert.deepEqual(told, [], "first seen"); trunks["novox/mesh-host:main"] = h("4"); await weighTrunks(forge as any, gates as any, trunks, "novox", "mesh-host", async (b) => { told.push(b); }); assert.deepEqual(told, ["main"]); }); // --- the merge rule ------------------------------------------------------------------------------------------- test("the group rule names every missing thing: the group status at each head read, and each sibling head's review", async () => { const self = { owner: "novox", repo: "mesh-controller", number: 346, head: MEMBER_HEAD }; const statuses: Record = {}; const approved = new Set(); const forge = { headChecks: async (_o: string, r: string, sha: string) => (statuses[`${r}@${sha}`] ? { "mesh/delivery-group": { state: statuses[`${r}@${sha}`] } } : {}), approvedAt: async (_o: string, r: string, n: number, head: string) => approved.has(`${r}#${n}@${head}`), }; const siblings = siblingsOf([sibling]); const none = await groupRefusal(forge, self, siblings); assert.match(none!, /mesh\/delivery-group is missing at mesh-controller#346@aaaaaaaaaaaa; mesh\/delivery-group is missing at mesh-host#114@bbbbbbbbbbbb; mesh-host#114@bbbbbbbbbbbb carries no approving review at that head/); statuses[`mesh-controller@${MEMBER_HEAD}`] = "success"; statuses[`mesh-host@${HOST_HEAD}`] = "pending"; assert.match((await groupRefusal(forge, self, siblings))!, /^mesh\/delivery-group is pending at mesh-host#114@bbbbbbbbbbbb; mesh-host#114@bbbbbbbbbbbb carries no approving review/); statuses[`mesh-host@${HOST_HEAD}`] = "success"; assert.match((await groupRefusal(forge, self, siblings))!, /^mesh-host#114@bbbbbbbbbbbb carries no approving review at that head$/); approved.add(`mesh-host#114@${HOST_HEAD}`); assert.equal(await groupRefusal(forge, self, siblings), undefined); // A read that fails is named, never read as a pass. const broken = { ...forge, approvedAt: async () => { throw new Error("the forge is away"); } }; assert.match((await groupRefusal(broken, self, siblings))!, /could not be read \(the forge is away\)/); assert.equal(besideWords(siblings), "beside mesh-host#114@bbbbbbbb"); }); test("a merged or closed member's entry is dropped, so a sibling's later push never re-pends its merged head", async () => { // mesh-host#114 judged beside mesh-controller#346; #114 then merged; a push to #346 follows. const w = world(); w.host.merged = true; // the member (mesh-host#114) is merged w.member.head_sha = h("9"); // the sibling (mesh-controller#346) has a new head const record: SiblingRecord = { "novox/mesh-host#114": { head: HOST_HEAD, siblings: siblingsOf([ { repository: "novox/mesh-controller", number: 346, head: MEMBER_HEAD, base: "main", base_commit: h("2") }]) } }; assert.equal(await weigh(w, record, "novox/mesh-controller", 346), true); assert.deepEqual(w.written, [], "the merged head is not put back to pending"); assert.deepEqual(record, {}, "and its entry is gone"); // On its own close or merge the member's entry goes too, whatever the siblings do. const own = world(); own.host.state = "closed"; const kept = recordOf(); const mine: SiblingRecord = { "novox/mesh-host#114": { head: HOST_HEAD, siblings: kept["novox/mesh-controller#346"].siblings } }; assert.equal(await weigh(own, mine, "novox/mesh-host", 114), true); assert.deepEqual(mine, {}); assert.deepEqual(own.written, []); });