51 lines
3.3 KiB
TypeScript
51 lines
3.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { ViewFollower, viewTools, type ViewAnswer, type ViewChange, type ViewEvent } from "../views.ts";
|
|
|
|
// ADR 0615: the module asks the indexer's read tool once, then keeps its picture of `modules` from the indexer's views
|
|
// events; a gap asks once more; an owner that is down is asked again and never read instead.
|
|
|
|
const made = "2026-11-04T12:00:00Z";
|
|
const answer = (revision: number, values: Record<string, unknown>): ViewAnswer =>
|
|
({ v: 1, owner: "o", state: "modules", revision, created: made, values });
|
|
const ev = (revision: number, key: string, value?: unknown, extra: Partial<ViewEvent> = {}): ViewEvent =>
|
|
({ v: 1, owner: "o", state: "modules", key, revision, created: made, value, deleted: value === undefined, ...extra });
|
|
|
|
test("a follower takes the first view, then the events, and asks again only on a gap", async () => {
|
|
const changes: ViewChange[] = [];
|
|
const said: string[] = [];
|
|
let asks = 0;
|
|
let down = 2;
|
|
const f = new ViewFollower({ owner: "o", state: "modules", tool: "o.read",
|
|
ask: async () => { if (down-- > 0) throw new Error("no responders"); asks++; return answer(9, { a: { repository: "novox/a" }, b: { repository: "novox/b" } }); },
|
|
take: (c) => { changes.push(c); }, say: (l) => said.push(l), sleep: async () => {}, now: (() => { let t = 0; return () => (t += 200_000); })() });
|
|
await f.start();
|
|
assert.equal(f.ready, true);
|
|
assert.equal(f.values.size, 2);
|
|
assert.ok(said.some((l) => l.includes("no first view of o's modules") && l.includes("o.read")), said.join("|"));
|
|
await f.hear(ev(8, "a", { repository: "old" })); // already in the picture
|
|
await f.hear(ev(10, "c", { repository: "novox/c" }));
|
|
await f.hear(ev(11, "a")); // deleted
|
|
assert.deepEqual([f.revision, f.values.has("a"), f.values.has("c"), asks], [11, false, true, 1]);
|
|
assert.deepEqual(changes.slice(-2).map((c) => [c.key, c.op]), [["c", "put"], ["a", "delete"]]);
|
|
await f.hear(ev(13, "d", { repository: "x" })); // revision 12 missed: a gap
|
|
assert.equal(asks, 2);
|
|
await f.hear(ev(1, "e", { repository: "x" }, { created: "2026-11-04T13:00:00Z" })); // the state made again: a gap
|
|
assert.equal(asks, 3);
|
|
await f.hear(ev(10, "f", undefined, { gap: true })); // the next revision, too large to carry: a gap
|
|
assert.equal(asks, 4);
|
|
await f.hear({ ...ev(10, "g", { repository: "x" }), state: "other" }); // another state
|
|
assert.equal(f.values.has("g"), false);
|
|
});
|
|
|
|
test("the reader's two tools say where the first view stands and ask it again by hand", async () => {
|
|
let asks = 0;
|
|
const follower = new ViewFollower({ owner: "o", state: "modules", tool: "o.read", ask: async () => { asks++; return answer(5, { a: 1 }); },
|
|
take: () => {}, sleep: async () => {} });
|
|
const [first, again] = viewTools(() => ({ follower, owner: "o", tool: "o.read", state: "modules" }));
|
|
assert.deepEqual(await first.run({}), { views: [{ owner: "o", tool: "o.read", state: "modules", revision: 0, created: "", keys: 0, answered: false, asks: 0, "by-hand": 0 }] });
|
|
const after = (await again.run({})) as { views: { revision: number; "by-hand": number; asks: number }[] };
|
|
assert.deepEqual([after.views[0].revision, after.views[0]["by-hand"], asks], [5, 1, 1]);
|
|
assert.deepEqual(await viewTools(() => undefined)[0].run({}), { views: [] });
|
|
});
|