The four share files (index.ts, module.json, guard.test.ts), so they land as one commit. GIT1: seat.ts serves pull, open-pulls, tags, trees, file, contains, statuses, status, comment, view, note, merge-pull as tools on the seat, each answering the contract's shape and an error of kind refused, not-found or failed; a comment, view or note whose act id is held is answered held (note matched by id). merge-pull merges the head the caller names (its from-G judgements are GIT6, GIT7, GIT9). GIT2: seatevents.ts says pull-updated, pull-merged, pull-closed, forge-pull, forge-status, forge-delete and repo-writable on mesh.seat.git.event.<event>; the module's emits drop them. GIT3: merge.ts, check.ts, the acting tools, the owed watch, the gate's source read and check-gates-merge are gone. GIT10: catalog.ts follows the seat mesh-catalog (revision, catalog-revision); no write is granted before the first view. Builds: design 47 G §310, §311, §312, §313, §314, §163, §317, §278, §321, §196 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
96 lines
5.4 KiB
TypeScript
96 lines
5.4 KiB
TypeScript
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<string, unknown>[] = [];
|
|
const trunk = new Map<number, { module: string; repository: string }[][]>([
|
|
[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<string, unknown>; 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<void>((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]);
|
|
});
|