Files
mesh-catalog/modules/gitea/test/gate.test.ts
T

206 lines
11 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 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";
/** 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");
// **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.
/** 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("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 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);
});
// Issue 1301: a build machine's start on a head that holds no mesh/merge-gate (its base requires none, so none was written)
// says nothing: no "building on" status appears on a main that does not require the gate.