import assert from "node:assert/strict"; import { test } from "node:test"; import { viewWaitsFinding } from "@novox/mesh-sdk/views"; import { CatalogView, REVISION_VERB, findingValue, revisionAnswer, unansweredFinding } from "../catalog.ts"; // Design 47 *Every part takes a first view, then follows the events*: the forge module asks the seat mesh-catalog's verb // `revision` for the catalog half (every page of one trunk revision), then hears its event `catalog-revision`. A candidate's // revision moves nothing; a newer trunk revision is waited for, said, and asked; waited for past the bound, view-waits is // written, and cleared once held; an owner that does not answer is asked again and raised past its bound. const entry = (module: string, repository: string) => ({ module, repository, dir: ".", manifest: "m", fingerprint: "f", verdict: "accepted" }); const revisionOf = (revision: number, kind = "trunk") => ({ revision, kind, source: revision, entries: [] }); /** A holder of the seat whose trunk revisions are pages of entries, and who answers what is asked of it. */ function aHolder() { const asked: Record[] = []; const trunk = new Map([ [7, [[entry("a", "u/a"), entry("b", "u/b")], [entry("c", "u/c")]]], [8, [[entry("a", "u/a"), entry("c", "u/c2")]]], ]); let newest = 7; let down = 0; return { asked, set newest(n: number) { newest = n; }, set down(n: number) { down = n; }, ask: async (verb: string, input: { after?: string; revision?: number }) => { assert.equal(verb, REVISION_VERB); asked.push({ ...input }); if (down > 0) { down--; return { error: "no responders" }; } const rev = input.revision ?? newest; const pages = trunk.get(rev)!; const at = input.after ? pages.findIndex((p) => p[0].module > input.after!) : 0; return { revision: revisionOf(rev), entries: pages[at], ...(at < pages.length - 1 ? { next: pages[at].at(-1)!.module } : {}) }; }, }; } test("the catalog view takes the trunk's first view page by page, then a newer trunk revision, and a candidate's moves nothing", async () => { const holder = aHolder(); const changes: string[] = []; const view = new CatalogView(holder.ask, { pause: async () => {}, changed: (k, v, deleted) => { changes.push(deleted ? `-${k}` : `${k}=${(v as any).repository}`); } }); await view.follower.start(); assert.deepEqual(holder.asked, [{}, { after: "b", revision: 7 }], "the second page names the first page's revision"); assert.deepEqual([view.held, view.follower.status().view, changes.sort()], [7, "7", ["a=u/a", "b=u/b", "c=u/c"]]); const rows: { name: string; ev: Record; asks: number; held: number }[] = [ { name: "a candidate's revision", ev: { v: 1, kind: "candidate", revision: 9, source: 8, group: "g", heads: "h" }, asks: 0, held: 7 }, { name: "the revision it holds", ev: { v: 1, kind: "trunk", revision: 7, source: 7 }, asks: 0, held: 7 }, { name: "a newer trunk revision", ev: { v: 1, kind: "trunk", revision: 8, source: 8 }, asks: 1, held: 8 }, ]; for (const r of rows) { holder.newest = 8; holder.asked.splice(0); changes.splice(0); await view.hear(r.ev as any); await view.asked; assert.deepEqual([holder.asked.length, view.held], [r.asks, r.held], r.name); } assert.deepEqual(changes.sort(), ["-b", "c=u/c2"], "the newer revision hands what changed"); const s = view.follower.status(); assert.deepEqual([s.view, s["waits-for"]], ["8", undefined], "it holds what it waited for"); await assert.rejects(view.hear({ v: 1, kind: "trunk" } as any), /not a mesh-catalog.catalog-revision event/); assert.throws(() => revisionAnswer({ error: "refused for gitea" }), /answered no revision: refused for gitea/); }); test("a revision waited for past the bound writes view-waits, cleared once held; an unanswered first view is raised, then cleared", async () => { const holder = aHolder(); const said: string[] = []; let bound = () => {}; let clock = 0; const view = new CatalogView(holder.ask, { pause: () => new Promise((r) => setImmediate(r)), bound: 1000, now: () => new Date(Date.parse("2026-11-09T22:00:00Z") + (clock += 600)), waitsBound: () => new Promise((r) => (bound = r)), raise: (s) => { said.push(`raised ${findingValue(unansweredFinding("gitea", s, new Date())).key}`); }, clear: () => { said.push("cleared"); }, raiseWaits: (s) => { said.push(`waits ${findingValue(viewWaitsFinding("gitea", s, new Date())).key}`); }, clearWaits: () => { said.push("waits cleared"); }, }); holder.down = 3; await view.follower.start(); assert.deepEqual(said, ["raised module.gitea.mesh-catalog.catalog-revisions.first-view-unanswered", "cleared"]); said.splice(0); holder.newest = 8; holder.down = 100; // the holder is away when the newer revision is said const heard = view.hear({ v: 1, kind: "trunk", revision: 8, source: 8 } as any); await heard; assert.equal(view.follower.status()["waits-for"], "8", "it says what it waits for"); bound(); await new Promise((r) => setImmediate(r)); assert.deepEqual(said.filter((l) => l.startsWith("waits")), ["waits module.gitea.mesh-catalog.catalog-revisions.view-waits"]); holder.down = 0; await view.follower.gap("the holder is back"); await view.asked; assert.deepEqual([view.held, view.follower.status()["waits-for"], said.includes("waits cleared")], [8, undefined, true]); });