Files
mesh-catalog/modules/gitea/test/gate.test.ts
T
jschoubben 5053e96d4e
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
Route a recheck's pending through the gitea module's one writer of mesh/merge-gate
The recheck's pending was written from the tools' bundle, its own process, past the
writer's per-head chain: the first run's verdict mark stayed set, so a rechecked head
never said where its new run builds, and an old run's verdict in flight or redelivered
could land over the pending. The status tool is now served from the watcher's bundle
and writes the merge check's pending through the writer, and a verdict whose ask
(checked.id) predates the check now pending is dropped, weighed after a restart against
the forge's own history of the gate (novox/hq issue 456).
2026-10-12 12:20:23 +02:00

325 lines
18 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
// **A pending check says where it is, and the verdict lands last** (novox/hq issue 446). This module is the one writer
// of a head's mesh/merge-gate: the announcement's pending words, "building on <machine> since …" when the build seat
// says a machine took the check, and the verdict. The forge keeps the newest status of a context, so a pending word
// written after the verdict hides it — a pass reads pending, a failure hides under "building".
const SHA = "a6b08de8507c1703528277e5cefc80548dbc9bb3";
// The build seat's `started` as a machine said it for novox/hq#309's check on 2026-10-11 (its log, read with the
// controller's `builds --log build-1791680941048622697`, opens at 03:09:01 on shanks); only the forge's address
// replaced. The shape is mesh-controller's link.BuildStart.
const startedOnShanks = { id: "build-1791680941048622697", repository: "http://127.0.0.1:20000/novox/hq.git",
ref: SHA, on: "shanks", at: "2026-10-11T01:09:01.204319Z" };
/** A forge that keeps the newest status per context, as gitea does, and whose writes take as long as a test says. */
function aForge() {
const kept: Record<string, { state: string; description: string; updated_at: string }> = {};
const written: string[] = [];
/** Every mesh/merge-gate status the forge was given, newest first, as gitea keeps each one. */
const history: { state: string; description: string; updated_at: string }[] = [];
let failNext = 0;
let hold: Promise<void> | null = null;
let release = () => {};
let readHold: Promise<void> | null = null;
let readRelease = () => {};
return {
kept, written, history,
/** The next writes fail: a request the forge did not take. */
failWrites(n = 1) {
failNext = n;
},
/** The next writes wait until released: a request in flight. */
holdWrites() {
hold = new Promise<void>((r) => (release = r));
},
release() {
hold = null;
release();
},
async setCommitStatus(_o: string, _r: string, _sha: string, s: { state: string; context: string; description: string }) {
if (hold) await hold;
if (failNext > 0) {
failNext--;
throw new Error("the forge answered 502");
}
kept[s.context] = { state: s.state, description: s.description, updated_at: new Date().toISOString() };
if (s.context === "mesh/merge-gate") history.unshift(kept[s.context]);
written.push(`${s.state} ${s.description}`);
},
/** Reads answer what the forge held when asked, as late as the reads are held: the answer of a request in flight. */
holdReads() {
readHold = new Promise<void>((r) => (readRelease = r));
},
releaseReads() {
readHold = null;
readRelease();
},
async gateOf(_o: string, _r: string, _sha: string) {
const held = kept["mesh/merge-gate"] ? { ...kept["mesh/merge-gate"] } : undefined;
if (readHold) await readHold;
return held;
},
async gateHistory(_o: string, _r: string, _sha: string) {
return history.map((h) => ({ ...h }));
},
};
}
/** The clock of the test: the head announced at 01:08:50Z. */
const announcedAt = () => new Date("2026-10-11T01:08:50Z");
test("a build machine taking the check is said on the head: building on which machine, since the local time", async () => {
const { GateWriter, buildingWords } = await import("../gate.ts");
const forge = aForge();
const gates = new GateWriter(forge, announcedAt);
await gates.pending("novox", "hq", SHA);
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), true);
assert.equal(forge.kept["mesh/merge-gate"].state, "pending");
assert.equal(forge.kept["mesh/merge-gate"].description, buildingWords("shanks", new Date(startedOnShanks.at)));
assert.equal(buildingWords("shanks", new Date(2026, 9, 11, 3, 9, 1)), "building on shanks since 03:09");
});
test("the verdict heard while a 'building' write is in flight lands last and stays", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
const gates = new GateWriter(forge, announcedAt);
await gates.pending("novox", "hq", SHA);
forge.holdWrites(); // the 'building' request is in flight when the verdict comes
const building = gates.started("novox", "hq", SHA, startedOnShanks);
await new Promise((r) => setImmediate(r));
const verdict = gates.verdict("novox", "hq", SHA, () =>
forge.setCommitStatus("novox", "hq", SHA, { state: "success", context: "mesh/merge-gate", description: "pass" }));
forge.release();
await Promise.all([building, verdict]);
assert.equal(`${forge.kept["mesh/merge-gate"].state} ${forge.kept["mesh/merge-gate"].description}`, "success pass",
`the verdict is not the newest status: ${forge.written.join(" | ")}`);
});
// The race the review of mesh-catalog#202 reproduced: a writer reads the forge's gate (pending), the verdict is written,
// then the writer writes "building" — and the pass reads pending until the next push. A head this process did not
// announce is the one case where the forge is read before writing.
test("the verdict written while a start's read of the forge is in flight stays the newest status", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
forge.kept["mesh/merge-gate"] = { state: "pending", description: "waiting", updated_at: "2026-10-11T01:08:50Z" };
const gates = new GateWriter(forge, announcedAt);
forge.holdReads();
const building = gates.started("novox", "hq", SHA, startedOnShanks);
await new Promise((r) => setImmediate(r));
const verdict = gates.verdict("novox", "hq", SHA, () =>
forge.setCommitStatus("novox", "hq", SHA, { state: "success", context: "mesh/merge-gate", description: "pass" }));
await new Promise((r) => setImmediate(r));
forge.releaseReads();
await Promise.all([building, verdict]);
assert.equal(`${forge.kept["mesh/merge-gate"].state} ${forge.kept["mesh/merge-gate"].description}`, "success pass",
`the verdict is not the newest status: ${forge.written.join(" | ")}`);
});
test("the verdict heard while a 'building' write still waits its turn: the 'building' is never written", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
const gates = new GateWriter(forge, announcedAt);
forge.holdWrites(); // the pending words are still being written
const pending = gates.pending("novox", "hq", SHA);
const building = gates.started("novox", "hq", SHA, startedOnShanks);
const verdict = gates.verdict("novox", "hq", SHA, () =>
forge.setCommitStatus("novox", "hq", SHA, { state: "failure", context: "mesh/merge-gate", description: "fail: 0 of 4 compose" }));
forge.release();
await Promise.all([pending, verdict]);
assert.equal(await building, false);
assert.equal(forge.kept["mesh/merge-gate"].state, "failure");
assert.ok(!forge.written.some((w) => w.includes("building")), forge.written.join(" | "));
});
test("a start heard after the verdict writes nothing", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
const gates = new GateWriter(forge, announcedAt);
await gates.pending("novox", "hq", SHA);
await gates.verdict("novox", "hq", SHA, () =>
forge.setCommitStatus("novox", "hq", SHA, { state: "success", context: "mesh/merge-gate", description: "pass" }));
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false);
assert.equal(forge.kept["mesh/merge-gate"].state, "success");
});
test("a start from before the check now pending (a run before a recheck, the stream's history) writes nothing", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
let now = announcedAt();
const gates = new GateWriter(forge, () => now);
await gates.pending("novox", "hq", SHA);
await gates.verdict("novox", "hq", SHA, () =>
forge.setCommitStatus("novox", "hq", SHA, { state: "failure", context: "mesh/merge-gate", description: "error" }));
now = new Date("2026-10-11T01:30:00Z"); // checked again: the recheck's pending, as the tool writes it (issue 456)
await gates.rechecked("novox", "hq", SHA, { state: "pending", context: "mesh/merge-gate", description: "checking again: flaky" });
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false, "the first run's start");
assert.equal(forge.kept["mesh/merge-gate"].description, "checking again: flaky");
assert.equal(await gates.started("novox", "hq", SHA, { ...startedOnShanks, at: "2026-10-11T01:31:00Z" }), true,
"the second run's start");
});
// **A recheck's pending goes through the one writer** (novox/hq issue 456). mesh-delivery's recheck puts the gate back
// to pending through the gitea_commit_status tool; before, that write bypassed the head's chain: the first run's
// verdict mark stayed set, so the new run never said "building", and an old run's verdict could land over the pending.
/** The ask of a check, as the controller's link.NewBuildID names it: when it was asked, in unix nanoseconds. */
const askOf = (at: string) => `build-${BigInt(Date.parse(at)) * 1_000_000n}`;
const firstRun = askOf("2026-10-11T01:09:01Z");
const recheckAt = new Date("2026-10-11T01:30:00Z");
const secondRun = askOf("2026-10-11T01:30:02Z");
const checkingAgain = { state: "pending" as const, context: "mesh/merge-gate", description: "checking again: main moved on" };
const writeOf = (forge: ReturnType<typeof aForge>, state: string, description: string) => () =>
forge.setCommitStatus("novox", "hq", SHA, { state, context: "mesh/merge-gate", description });
const gateOf = (forge: ReturnType<typeof aForge>) =>
`${forge.kept["mesh/merge-gate"].state} ${forge.kept["mesh/merge-gate"].description}`;
test("an old run's verdict redelivered after a recheck's pending is not written over it", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
let now = announcedAt();
const gates = new GateWriter(forge, () => now);
await gates.pending("novox", "hq", SHA);
forge.failWrites(); // the first run's verdict is not taken: the event is offered again
await assert.rejects(gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun));
now = recheckAt;
await gates.rechecked("novox", "hq", SHA, checkingAgain);
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun); // redelivered
assert.equal(gateOf(forge), "pending checking again: main moved on", forge.written.join(" | "));
// The recheck's own verdict is written.
await gates.verdict("novox", "hq", SHA, writeOf(forge, "failure", "fail: 0 of 4 compose"), secondRun);
assert.equal(gateOf(forge), "failure fail: 0 of 4 compose");
});
test("an old run's verdict in flight when the recheck comes lands before the recheck's pending", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
let now = announcedAt();
const gates = new GateWriter(forge, () => now);
await gates.pending("novox", "hq", SHA);
forge.holdWrites(); // the first run's verdict is being written when the recheck comes
const verdict = gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
await new Promise((r) => setImmediate(r));
now = recheckAt;
const pending = gates.rechecked("novox", "hq", SHA, checkingAgain);
forge.release();
await Promise.all([verdict, pending]);
assert.equal(gateOf(forge), "pending checking again: main moved on", forge.written.join(" | "));
});
test("a rechecked head says where its new run is: building on which machine", async () => {
const { GateWriter, buildingWords } = await import("../gate.ts");
const forge = aForge();
let now = announcedAt();
const gates = new GateWriter(forge, () => now);
await gates.pending("novox", "hq", SHA);
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
now = recheckAt;
await gates.rechecked("novox", "hq", SHA, checkingAgain);
const at = "2026-10-11T01:30:03Z";
assert.equal(await gates.started("novox", "hq", SHA, { ...startedOnShanks, id: secondRun, at }), true);
assert.equal(forge.kept["mesh/merge-gate"].description, buildingWords("shanks", new Date(at)));
});
test("the recheck's own verdict is written, even asked a little before the pending by another machine's clock", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
const gates = new GateWriter(forge, () => recheckAt);
await gates.rechecked("novox", "hq", SHA, checkingAgain);
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), askOf("2026-10-11T01:29:30Z"));
assert.equal(gateOf(forge), "success pass");
// A verdict whose ask cannot be read is written: a verdict is never lost to what cannot be weighed.
await gates.rechecked("novox", "hq", SHA, checkingAgain);
await gates.verdict("novox", "hq", SHA, writeOf(forge, "failure", "fail"), "an id of another shape");
assert.equal(gateOf(forge), "failure fail");
});
test("after a restart, an old run's verdict is weighed against the check the forge's history holds open", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
// What the forge kept before this process started: the announcement, the first run's verdict never written,
// the recheck's pending, and the second run's "building" over it.
forge.history.unshift({ state: "pending", description: "waiting in the build queue since 03:08", updated_at: "2026-10-11T01:08:50Z" });
forge.history.unshift({ state: "pending", description: "checking again: main moved on", updated_at: "2026-10-11T01:30:00Z" });
forge.history.unshift({ state: "pending", description: "building on shanks since 03:30", updated_at: "2026-10-11T01:30:05Z" });
forge.kept["mesh/merge-gate"] = forge.history[0];
const gates = new GateWriter(forge, announcedAt);
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
assert.equal(forge.kept["mesh/merge-gate"].description, "building on shanks since 03:30", forge.written.join(" | "));
await gates.verdict("novox", "hq", SHA, writeOf(forge, "failure", "fail"), secondRun);
assert.equal(gateOf(forge), "failure fail");
});
test("a recheck the forge does not take fails its caller, and the head is as it was", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
let now = announcedAt();
const gates = new GateWriter(forge, () => now);
await gates.pending("novox", "hq", SHA);
forge.failWrites();
await assert.rejects(gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun));
now = recheckAt;
forge.failWrites();
await assert.rejects(gates.rechecked("novox", "hq", SHA, checkingAgain));
// The recheck was refused, so its check is not asked: the first run's verdict, offered again, is the head's.
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
assert.equal(gateOf(forge), "success pass");
});
test("the status tool writes the merge check's pending through the writer, and the mesh's other statuses as they were", async () => {
const { GateWriter } = await import("../gate.ts");
const { commitStatusTool } = await import("../delivery.ts");
const forge = aForge();
const direct: string[] = [];
const gates = new GateWriter(forge, () => recheckAt);
const tool = commitStatusTool({
setCommitStatus: async (_o, _r, _sha, s) => void direct.push(`${s.context} ${s.state}`),
gates,
});
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
await tool.run({ owner: "novox", repo: "hq", sha: SHA, context: "mesh/merge-gate", state: "pending", description: "checking again: x" });
await tool.run({ owner: "novox", repo: "hq", sha: SHA, context: "mesh/repo-check", state: "pending", description: "checking again: x" });
await tool.run({ owner: "novox", repo: "hq", sha: SHA, context: "mesh/delivery", state: "success", description: "delivered" });
assert.equal(gateOf(forge), "pending checking again: x");
assert.equal(forge.kept["mesh/repo-check"].state, "pending");
assert.deepEqual(direct, ["mesh/delivery success"]);
// The redelivered first run's verdict is the writer's to drop.
await gates.verdict("novox", "hq", SHA, writeOf(forge, "success", "pass"), firstRun);
assert.equal(gateOf(forge), "pending checking again: x");
// A head named short is another head to the writer: refused, nothing sent.
await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: SHA.slice(0, 12), context: "mesh/merge-gate", state: "pending" }),
/whole sha/);
await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: SHA, context: "mesh/merge-gate", state: "success" }),
/set by its verdict only/);
});
test("when a check was asked is read from its id; the opening of the check now pending from the gate's history", async () => {
const { askedAt, openedAt } = await import("../gate.ts");
assert.equal(askedAt("build-1791680941048622697"), Date.parse("2026-10-11T01:09:01.048Z"));
assert.equal(askedAt("build-mesh-catalog"), undefined);
assert.equal(askedAt(undefined), undefined);
assert.equal(openedAt([{ state: "pending", description: "building on ace since 03:31", updated_at: "2026-10-11T01:31:00Z" },
{ state: "pending", description: "checking again: x", updated_at: "2026-10-11T01:30:00Z" }]), Date.parse("2026-10-11T01:30:00Z"));
assert.equal(openedAt([{ state: "success", description: "pass", updated_at: "2026-10-11T01:30:00Z" }]), undefined);
assert.equal(openedAt([]), undefined);
});
test("a head this process did not announce (it restarted) is said only while the forge holds its gate pending", async () => {
const { GateWriter } = await import("../gate.ts");
const forge = aForge();
forge.kept["mesh/merge-gate"] = { state: "success", description: "pass", updated_at: "2026-10-11T01:20:00Z" };
const gates = new GateWriter(forge, announcedAt);
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), false);
forge.kept["mesh/merge-gate"] = { state: "pending", description: "waiting", updated_at: "2026-10-11T01:08:50Z" };
assert.equal(await gates.started("novox", "hq", SHA, startedOnShanks), true);
});
test("a repository as the mesh clones it is told from another", async () => {
const { clonedAs } = await import("../gate.ts");
assert.equal(clonedAs("http://127.0.0.1:20000/novox/hq.git", "novox", "hq"), true);
assert.equal(clonedAs("http://127.0.0.1:20000/novox/mesh-hq.git", "novox", "hq"), false);
assert.equal(clonedAs("http://127.0.0.1:20000/novox/mesh-controller.git", "novox", "hq"), false);
});