| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { DesktopEventStream, MissedEventSubscriptions, type EventRecovery } from "./eventStream.js"; |
| 4 | import type { EventFrame } from "./ipc.js"; |
| 5 | |
| 6 | test("missed dynamic subscriptions have bounded retention and overflow still requests recovery", () => { |
| 7 | const missed = new MissedEventSubscriptions(); |
| 8 | missed.add("agent:event"); |
| 9 | assert.equal(missed.consume("agent:event"), true); |
| 10 | assert.equal(missed.consume("agent:event"), false, "normal recovery consumes the pending name"); |
| 11 | for (let index = 0; index < 10000; index++) { |
| 12 | missed.add(`remote-tab:closed-${index}:event`); |
| 13 | assert.ok(missed.size <= 64); |
| 14 | } |
| 15 | assert.equal(missed.size, 0, "overflow retains no closed-tab event names"); |
| 16 | assert.equal(missed.consume("remote-tab:closed-0:event"), true, "an evicted name still triggers read-side repair"); |
| 17 | assert.equal(missed.consume("remote-tab:closed-9999:event"), true); |
| 18 | missed.clear(); |
| 19 | assert.equal(missed.consume("remote-tab:closed-0:event"), false, "a new generation drops old pending state"); |
| 20 | missed.add("agent:event"); |
| 21 | assert.equal(missed.size, 1); |
| 22 | }); |
| 23 | |
| 24 | test("transport rejects stale generations and duplicate/out-of-order frames and reports sequence gaps", () => { |
| 25 | const accepted: EventFrame[] = [], repairs: EventRecovery[] = []; |
| 26 | const stream = new DesktopEventStream(frame => accepted.push(frame), event => repairs.push(event)); |
| 27 | const frame = (seq: number, generation = "g1", name = "agent:event") => ({ seq, generation, name, args: [seq] }); |
| 28 | assert.equal(stream.accept(frame(1)), false, "unnegotiated generations cannot enter the renderer"); |
| 29 | stream.observeState({ phase: "ready", generation: "g1" }); |
| 30 | assert.equal(stream.accept(frame(1)), true); |
| 31 | assert.equal(stream.accept(frame(1)), false); |
| 32 | assert.equal(stream.accept(frame(3)), true); |
| 33 | assert.equal(stream.accept(frame(2)), false); |
| 34 | assert.equal(stream.accept(frame(4, "old")), false); |
| 35 | assert.equal(stream.accept(frame(Number.NaN)), false); |
| 36 | assert.equal(stream.accept(frame(0)), false); |
| 37 | assert.equal(stream.accept(frame(0, "g1", "app:open-settings")), true, "shell events have their own sequence-free namespace"); |
| 38 | assert.deepEqual(repairs.map(({ reason, expectedSeq, actualSeq }) => ({ reason, expectedSeq, actualSeq })), [ |
| 39 | { reason: "generation", expectedSeq: 1, actualSeq: 0 }, |
| 40 | { reason: "gap", expectedSeq: 2, actualSeq: 3 }, |
| 41 | ]); |
| 42 | stream.observeState({ phase: "restarting", generation: "" }); |
| 43 | assert.equal(stream.accept(frame(4)), false); |
| 44 | stream.observeState({ phase: "ready", generation: "g2" }); |
| 45 | assert.equal(stream.accept(frame(5)), false, "queued frames from the previous service stay rejected"); |
| 46 | assert.equal(stream.accept(frame(1, "g2")), true); |
| 47 | assert.deepEqual(accepted.map(value => `${value.generation}:${value.seq}`), ["g1:1", "g1:3", "g1:0", "g2:1"]); |
| 48 | stream.requestRecovery("subscription"); |
| 49 | assert.equal(stream.recovery?.reason, "subscription"); |
| 50 | assert.equal(stream.recovery?.generation, "g2"); |
| 51 | }); |
| 52 |