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

124 lines
8.3 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, () => {}, () => false, async () => ({ bytes: 4321, keys: 2, max_bytes: 64 * 1024 * 1024 }));
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.bytes, shown.fill.cap],
[[["nats.fp-1.unit", "main.7.nats-22"]], 1, 2, 4321, 64 * 1024 * 1024], "the fill is the state call's answer, not counted from the keys");
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"]);
});