The seat mesh-pipeline-runner's step event carries `fingerprint`; gitea read `fp`, which no event carries, and which the type-check refuses once the Node fetch places the runner's contracts (design 47 G A-285). Builds: design 47 G A-285 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
124 lines
8.1 KiB
TypeScript
124 lines
8.1 KiB
TypeScript
// 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<string, unknown>();
|
|
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<string, unknown> = {}, over: Record<string, unknown> = {}) => ({
|
|
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"]);
|
|
});
|