// The forge module judges a head by the failed steps it heard (design 47 §39, §475, §487, §548, §135): what the runner's // step-ended writes, what a passing rerun clears, which head is refused, and what goes once no head names a commit. import assert from "node:assert/strict"; import { test } from "node:test"; import { FailedSteps, Heads, failedStepKey, seenKey } from "../failedsteps.js"; const C1 = "1".repeat(40), C2 = "2".repeat(40), C3 = "3".repeat(40); function memory() { const kv = new Map(); return { kv, st: { get: async (k: string) => (kv.has(k) ? { key: k, value: kv.get(k), revision: 1 } : null), put: async (k: string, v: unknown) => { kv.set(k, structuredClone(v)); return 1; }, delete: async (k: string) => { kv.delete(k); }, keys: async () => [...kv.keys()].sort(), watch: async () => {}, } }; } const ended = (commit: string, outcome: string, extra: Record = {}, over: Record = {}) => ({ v: 1, pipeline: `main.7.nats-${commit.slice(0, 2)}`, module: "nats", branch: "main", commit, at: "2026-11-11T12:00:00Z", result: { step: "unit", status: "required", outcome, fingerprint: "fp-1", ...extra }, ...over }) as any; test("a head is refused by a required step that failed at its fingerprint in any run, until a rerun passes it", async () => { const { kv, st } = memory(); const heads = new Heads(); const f = new FailedSteps(st as any, () => {}, (c) => heads.has(c)); const notes: [string, string | null][] = []; const judged = async (name: string, commit: string, want: string | null) => { const got = await f.judge(commit); assert.equal(got ? `${got.run} ${got.module} ${got.step}` : null, want, name); }; await f.stepEnded(ended(C1, "passed")); await judged("a head that passed alone merges", C1, null); await judged("a head no run named merges", C3, null); await f.stepEnded(ended(C2, "failed", {}, { pipeline: "run-on-request" })); await judged("the head that failed", C2, "run-on-request nats unit"); await judged("an earlier commit of the same content (passed before, failed after)", C1, "run-on-request nats unit"); assert.deepEqual((kv.get(failedStepKey("nats", "fp-1", "unit")) as any).commits, [C1, C2], "the commits it was seen at"); // A step-ended of an ask that is no pipeline's step (no commit, no fingerprint) writes nothing (§548). const before = kv.size; await f.stepEnded({ v: 1, asker: "mesh-build-go", ask: "a-1", node: "ace", result: { step: "unit", status: "required", outcome: "failed" } } as any); assert.equal(kv.size, before, "a builder's failed build is no run of a commit"); // A later commit at the fingerprint is refused too, and a failed optional step refuses nothing. await f.stepEnded(ended(C3, "passed")); await judged("a later commit of the same content", C3, "run-on-request nats unit"); await f.stepEnded(ended(C3, "failed", { step: "lint", status: "optional", fingerprint: "fp-2" })); await judged("a failed optional step", C3, "run-on-request nats unit"); assert.equal(kv.has(failedStepKey("nats", "fp-2", "lint")), false); // A result that names no fingerprint is kept at the one the seat's step event carries (`fingerprint`). await f.stepEnded(ended(C3, "failed", { step: "lint", fingerprint: undefined }, { fingerprint: "fp-3" })); assert.ok(kv.has(failedStepKey("nats", "fp-3", "lint")), "the event's fingerprint is kept"); // A plain pass clears nothing; a rerun that passes clears it. await f.stepEnded(ended(C2, "passed")); await judged("a plain pass", C2, "run-on-request nats unit"); await f.stepEnded(ended(C2, "passed", { rerun: true, by: "operator" })); await judged("a passing rerun", C2, null); await judged("a passing rerun, for the earlier commit too", C1, null); void notes; }); test("an entry goes once none of its commits is a watched branch's head or an open pull request's head", async () => { const { kv, st } = memory(); const heads = new Heads(); const f = new FailedSteps(st as any, () => {}, (c) => heads.has(c)); heads.replace(new Set(["novox/app@main"]), new Map([["novox/app@main", C1]]), new Map([["novox/app#4", C2]])); await f.stepEnded(ended(C2, "failed")); await f.stepEnded(ended(C1, "passed")); assert.deepEqual([...kv.keys()].sort(), [failedStepKey("nats", "fp-1", "unit"), seenKey(C1, "nats"), seenKey(C2, "nats")].sort()); // The pull request is closed: its commit is no head, its seen key goes; the entry stays for the trunk's head that shares it. await f.lost(heads.pullEnded({ repository: "novox/app", number: 4 } as any)); assert.equal(kv.has(seenKey(C2, "nats")), false); assert.deepEqual((kv.get(failedStepKey("nats", "fp-1", "unit")) as any).commits, [C1]); // The branch moves on: nothing names the entry's commits, so it goes. A branch no registration watches moves nothing. assert.deepEqual(heads.branchMoved("novox/app", "feature", C3), []); await f.lost(heads.branchMoved("novox/app", "main", C3)); assert.deepEqual([...kv.keys()], [], "the failure and the seen keys are gone"); }); test("git_failed_steps shows what is held and its fill, and git_failed_steps_drop writes the hand act before it drops", async () => { const { failedStepsTools } = await import("../tools/failedsteps.js"); const { runAs } = await import("@novox/mesh-sdk/callers"); const { st } = memory(); const f = new FailedSteps(st as any); await f.stepEnded(ended(C2, "failed")); const acts: any[] = []; let recorded = true; const [read, drop] = failedStepsTools(f, async (a) => { if (!recorded) throw new Error("no hand-act log"); acts.push(a); }); const shown: any = await read.run({}); assert.deepEqual([shown.failed.map((x: any) => [x.key, x.run]), shown.seen.length, shown.fill.keys, shown.fill.cap], [[["nats.fp-1.unit", "main.7.nats-22"]], 1, 2, 64 * 1024 * 1024]); assert.equal(((await read.run({ commit: C1 })) as any).failed.length, 0, "a commit asked for filters"); recorded = false; await assert.rejects(runAs({ "mesh/caller": "operator" }, {}, () => drop.run({ key: "nats.fp-1.unit", why: "stale" })), /hand act was not recorded, so nothing was dropped/); assert.equal((await f.judge(C2))?.step, "unit", "nothing dropped without the hand act"); recorded = true; const done: any = await runAs({ "mesh/caller": "operator" }, {}, () => drop.run({ key: "nats.fp-1.unit", why: "stale" })); assert.deepEqual([done.dropped, acts.length, acts[0].verb], [true, 1, "git_failed_steps_drop"]); assert.equal(await f.judge(C2), null); }); test("a seen key past 30 days is deleted at the next step-ended, a younger one kept, at most 100 oldest first, and git_seen_expire runs the same now (§581)", async () => { const { failedStepsTools } = await import("../tools/failedsteps.js"); const { runAs } = await import("@novox/mesh-sdk/callers"); const { st, kv } = memory() as any; const f = new FailedSteps(st as any); // The step-ended is at 2026-11-11T12:00:00Z, so 30 days before it is 2026-10-12. const now = new Date("2026-11-11T12:00:00Z"); const old = (n: number) => new Date(Date.parse("2026-10-01T00:00:00Z") - n * 60000).toISOString(); await st.put(seenKey(C1, "old"), { v: 1, commit: C1, module: "old", fingerprints: ["x"], at: old(0) }); await st.put(seenKey(C2, "young"), { v: 1, commit: C2, module: "young", fingerprints: ["x"], at: "2026-10-20T00:00:00Z" }); await f.stepEnded(ended(C3, "passed")); assert.equal(kv.has(seenKey(C1, "old")), false, "past 30 days: deleted at the next step-ended"); assert.equal(kv.has(seenKey(C2, "young")), true, "younger: kept"); assert.equal(kv.has(seenKey(C3, "nats")), true, "the step's own key is new"); for (let i = 0; i < 101; i++) await st.put(seenKey(C1, `m${String(i).padStart(3, "0")}`), { v: 1, commit: C1, module: `m${i}`, fingerprints: ["x"], at: old(i) }); assert.equal((await f.expireSeen(now)).length, 100, "at most 100 for each event"); const acts: any[] = []; const tool = failedStepsTools(f, async (a) => { acts.push(a); }).find((t) => t.name === "git_seen_expire")!; const done: any = await runAs({ "mesh/caller": "operator" }, {}, () => tool.run({ why: "stale" })); assert.deepEqual([done.deleted.length, acts[0].verb], [1, "git_seen_expire"]); });