| 1 | import assert from "node:assert/strict"; |
| 2 | import { createRuntimeNotifications } from "../lib/runtimeNotifications"; |
| 3 | import { runtimeStateStore, type RuntimeSession } from "../lib/runtimeStateStore"; |
| 4 | import { t } from "../lib/i18n"; |
| 5 | |
| 6 | let alerts = 0; |
| 7 | let revision = 0; |
| 8 | const owner = createRuntimeNotifications(() => ({ activeTabId: "B", t, showToast() { alerts++; } })); |
| 9 | function session(index: number): RuntimeSession { |
| 10 | return { tabId: `detached:${index}`, scope: "global", workspaceRoot: "", topicId: "", sessionPath: "", |
| 11 | sessionGeneration: 1, open: false, remote: false, freshness: "synced", |
| 12 | state: { schemaVersion: 1, runtimeEpoch: `runtime:${index}`, activityRevision: 1, revision: 1, |
| 13 | phase: "executing", running: true, turnId: `turn:${index}`, turnStatus: "in_progress", turnEventSeq: 1, |
| 14 | pendingPrompt: true, pendingInteractions: [{ requestId: "1", kind: "ask", turnId: `turn:${index}`, |
| 15 | runtimeEpoch: `runtime:${index}`, headId: "head" }], |
| 16 | cancelRequested: false, cancellable: true, backgroundJobs: 0, activity: "" } }; |
| 17 | } |
| 18 | function publish(sessions: RuntimeSession[]) { |
| 19 | runtimeStateStore.commit({ epoch: "fixture", revision: ++revision, topics: [], sessions }); |
| 20 | } |
| 21 | publish([]); |
| 22 | owner.start(); |
| 23 | try { |
| 24 | publish([session(0)]); |
| 25 | assert.equal(alerts, 1); |
| 26 | for (let index = 1; index <= 600; index++) { |
| 27 | owner.accept({ event: { kind: "approval_request", tabId: "C", turnId: `other:${index}`, approval: { id: "1" } } }); |
| 28 | } |
| 29 | assert.equal(alerts, 601); |
| 30 | publish([session(0)]); |
| 31 | assert.equal(alerts, 601, "history eviction must not re-alert a still-pending background question"); |
| 32 | const manyPending = Array.from({ length: 600 }, (_, index) => session(index + 1000)); |
| 33 | publish(manyPending); |
| 34 | assert.equal(alerts, 1201); |
| 35 | publish(manyPending); |
| 36 | assert.equal(alerts, 1201, "more pending requests than the history cap cannot create an alert storm"); |
| 37 | publish(manyPending.map(item => ({ ...item, freshness: "unknown" }))); |
| 38 | publish(manyPending); |
| 39 | assert.equal(alerts, 1201, "disconnect and reconnect retain known pending identities beyond the history cap"); |
| 40 | owner.accept({ event: { kind: "ask_request", tabId: "reattached", turnId: "turn:1000", ask: { id: "1" } } }); |
| 41 | assert.equal(alerts, 1201, "wire replay also respects retained pending identities"); |
| 42 | publish([]); |
| 43 | publish([session(9000)]); |
| 44 | assert.equal(alerts, 1202, "a new pending request still alerts after the old requests resolve"); |
| 45 | console.log("runtime notifications: pending identities survive bounded history eviction and repeated large snapshots"); |
| 46 | } finally { owner.dispose(); } |
| 47 |