T-3: the finding writer in src/views (TypeScript twin), transport handed in

Builds: design 47 G §383, §384, §390, §439, §461, §485, §503

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 13:08:17 +01:00
parent ff7f38c757
commit 3cd4fea600
3 changed files with 439 additions and 0 deletions
+93
View File
@@ -833,3 +833,96 @@ test("a first view follows its owner's events, asks again on a gap and by hand,
assert.equal(wRaised.length, 1, "a view held before its bound raises nothing");
assert.equal(wCleared.length, 1, "nothing raised, nothing cleared");
});
test("the finding writer writes, clears and states findings as the Go writer does", async () => {
const { Writer, OBSERVE_RETRY_MS } = await import("../dist/views/index.js");
const store = new Map<string, any>();
const st: any = {
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
put: async (k: string, v: any) => (store.set(k, structuredClone(v)), 1),
delete: async (k: string) => void store.delete(k),
keys: async () => [...store.keys()].sort(),
};
let clock = Date.parse("2026-11-11T12:00:00Z");
const sent: { subject: string; body: any }[] = [];
let refuse = 0;
const waits: number[] = [];
let ask: ((b: unknown) => void) | undefined;
let consumers = 0;
let observes = 0;
const logs: string[] = [];
const w = new Writer({
module: "m",
state: st,
subjects: { written: "w", cleared: "c", stated: "s", asked: "a" },
observe: async () => {
if (++observes <= 2) throw new Error("not yet");
return { findings: [{ kind: "k", scope: "module", id: "a", severity: "warning", summary: "now", source: "p", subject: { scope: "module", id: "a" } }], pending: [{ key: "module.wait.k", since: "2026-11-11T11:00:00Z", bound: "5m" }] };
},
publish: async (subject, body) => {
if (refuse > 0) { refuse--; throw new Error("no ack"); }
sent.push({ subject, body });
},
listen: async (_s, h) => { consumers++; ask = h; return () => {}; },
log: (l) => logs.push(l),
now: () => new Date(clock),
wait: async (ms) => { waits.push(ms); clock += ms; return true; },
});
const f = { kind: "k", scope: "module", id: "a", severity: "warning" as const, summary: "one", source: "p", subject: { scope: "module", id: "a" } };
const first = new Date(clock).toISOString();
await w.write(f);
clock += 3600_000;
await w.write({ ...f, run: "other" }); // nothing it says changed
await w.write({ ...f, summary: "two" });
assert.deepEqual(sent.map((s) => s.subject), ["w", "w"]);
assert.equal(sent[1].body.since, first);
assert.equal(sent[1].body.observer, "m");
await w.clear("module", "never", "k", "held by the keeper");
assert.equal(sent[2].subject, "c");
assert.equal(sent[2].body.why, "held by the keeper");
// an unacknowledged event is sent again for a minute, then logged naming the finding
refuse = 1000;
const t0 = clock;
await w.write({ ...f, id: "b" });
assert.ok(clock - t0 >= 60_000 && clock - t0 < 70_000);
assert.ok(logs.some((l) => l.includes("module.b.k") && l.startsWith("error")));
refuse = 0;
// a start states what is observed now, retrying observe every minute, and deletes the keys not stated or pending
store.set("module.old.k", f);
waits.length = 0;
await w.start();
for (let i = 0; i < 50 && sent.length < 4; i++) await new Promise((r) => setImmediate(r));
const stated = sent[3];
assert.equal(stated.subject, "s");
assert.deepEqual(waits.slice(0, 2), [OBSERVE_RETRY_MS, OBSERVE_RETRY_MS]);
assert.equal(stated.body.findings.length, 1);
assert.equal(stated.body.findings[0].since, first);
assert.equal(stated.body.pending[0].key, "module.wait.k");
assert.deepEqual([...store.keys()].sort(), ["module.a.k"]);
// an ask for another module is not answered, one for this module is, with its id; a reconnect makes the consumer again
ask!({ ask: "a1", at: "", by: "keeper", module: "other" });
ask!({ ask: "a2", at: "", by: "keeper", module: "m" });
for (let i = 0; i < 50 && sent.length < 5; i++) await new Promise((r) => setImmediate(r));
assert.equal(sent.length, 5);
assert.equal(sent[4].body.asked, "a2");
await w.reconnected();
for (let i = 0; i < 50 && sent.length < 6; i++) await new Promise((r) => setImmediate(r));
assert.equal(consumers, 2);
assert.equal(sent.length, 6);
w.close();
// one act at a time: a write asked during a statement waits, so the statement never deletes the key it made
let release!: () => void;
const gate = new Promise<void>((r) => (release = r));
const w2 = new Writer({
module: "m", state: st, subjects: { written: "w", cleared: "c", stated: "s", asked: "a" },
observe: async () => { await gate; return { findings: [], pending: [] }; },
publish: async () => {}, listen: async () => () => {},
});
const stating = w2.state();
const writing = w2.write({ ...f, id: "during" });
for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r));
assert.equal(store.has("module.during.k"), false);
release();
await Promise.all([stating, writing]);
assert.equal(store.has("module.during.k"), true);
});