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:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user