| 1 | import assert from "node:assert/strict"; |
| 2 | import { TranscriptOutlineStore, type OutlineRead } from "../lib/transcriptOutlineStore"; |
| 3 | import type { HistoryOutlinePage, HistoryOutlineRequest } from "../generated/desktopContract.generated"; |
| 4 | |
| 5 | const requests: HistoryOutlineRequest[] = []; |
| 6 | const page = (start = 1): HistoryOutlinePage => ({ status: "ready", generation: "cut", snapshotSequence: 12, |
| 7 | coverageSequence: 12, totalTurns: 25000, nextTurn: start + 128, done: false, |
| 8 | entries: Array.from({ length: 128 }, (_, i) => ({ messageId: `u${start + i}`, turn: start + i, position: start + i, prompt: `question ${start + i}` })) }); |
| 9 | const read: OutlineRead = async (_tab, req) => { requests.push(req); return page(req.startTurn); }; |
| 10 | const store = new TranscriptOutlineStore(); |
| 11 | const off = store.activate("tab", "session", read, 25000); |
| 12 | await store.refresh("tab"); |
| 13 | assert.equal(store.getView("tab").totalTurns, 25000); |
| 14 | assert.equal(requests.length, 1, "initial read is one page, not the whole history"); |
| 15 | await Promise.all([store.ensure("tab", 20001), store.ensure("tab", 20001)]); |
| 16 | assert.equal(requests.length, 2, "range requests deduplicate"); |
| 17 | assert.equal(requests[1].generation, "cut"); |
| 18 | assert.equal(requests[1].snapshotSequence, 12); |
| 19 | for (let turn = 1000; turn < 10000; turn += 1000) await store.ensure("tab", turn); |
| 20 | assert.ok(store.getView("tab").entries.size <= 6 * 128, "summary cache is bounded"); |
| 21 | assert.equal(store.getView("tab").totalTurns, 25000, "eviction preserves the rail extent"); |
| 22 | await store.ensure("tab", 1); |
| 23 | assert.equal(store.getView("tab").entries.get(1)?.messageId, "u1"); |
| 24 | off(); |
| 25 | |
| 26 | let resolve!: (page: HistoryOutlinePage) => void; |
| 27 | const stale = new Promise<HistoryOutlinePage>(done => { resolve = done; }); |
| 28 | const oldOff = store.activate("same-tab", "old-session", () => stale, 2); |
| 29 | const pending = store.refresh("same-tab"); |
| 30 | await Promise.resolve(); |
| 31 | const newOff = store.activate("same-tab", "new-session", async () => ({ ...page(), totalTurns: 1, entries: [] }), 1); |
| 32 | await store.refresh("same-tab"); |
| 33 | resolve(page()); await pending; oldOff(); |
| 34 | assert.equal(store.getView("same-tab").totalTurns, 1, "old response and cleanup cannot replace the new session"); |
| 35 | newOff(); |
| 36 | |
| 37 | const failing = store.activate("failed", "session", async () => { throw new Error("network"); }, 5); |
| 38 | await store.refresh("failed"); |
| 39 | assert.equal(store.getView("failed").mode, "error"); |
| 40 | assert.equal(store.getView("failed").totalTurns, 5); |
| 41 | failing(); |
| 42 | const unsupported = store.activate("old-host", "session", async () => ({ ...page(), status: "unsupported", entries: [] }), 5); |
| 43 | await store.refresh("old-host"); |
| 44 | assert.equal(store.getView("old-host").mode, "unsupported"); unsupported(); |
| 45 | console.log("durable outline: sparse pages, fixed cut, eviction, generation and capability passed"); |
| 46 | |
| 47 | { |
| 48 | let fail = true; |
| 49 | const reads: number[] = []; |
| 50 | const cache = new TranscriptOutlineStore(); |
| 51 | const release = cache.activate("retry-page", "session", async (_tab, req) => { |
| 52 | reads.push(req.startTurn ?? 1); |
| 53 | if (req.startTurn === 129 && fail) throw new Error("page offline"); |
| 54 | return page(req.startTurn); |
| 55 | }, 25000); |
| 56 | try { |
| 57 | await cache.refresh("retry-page"); |
| 58 | await cache.ensure("retry-page", 129); |
| 59 | await cache.ensure("retry-page", 257); |
| 60 | assert.equal(cache.getView("retry-page").mode, "error", "another successful page cannot hide a failed page"); |
| 61 | fail = false; |
| 62 | await cache.retry("retry-page"); |
| 63 | assert.equal(cache.getView("retry-page").entries.get(129)?.messageId, "u129", "retry reloads the failed range even when the cut is unchanged"); |
| 64 | assert.equal(cache.getView("retry-page").mode, "ready"); |
| 65 | assert.equal(reads.filter(start => start === 129).length, 2); |
| 66 | } finally { release(); } |
| 67 | } |
| 68 | { |
| 69 | let cut = 12; |
| 70 | let finish!: (value: HistoryOutlinePage) => void; |
| 71 | const cache = new TranscriptOutlineStore(); |
| 72 | const release = cache.activate("cut-race", "session", async (_tab, req) => { |
| 73 | if (req.startTurn === 129 && req.snapshotSequence === 12) return new Promise(resolve => { finish = resolve; }); |
| 74 | return { ...page(req.startTurn), snapshotSequence: cut }; |
| 75 | }, 25000); |
| 76 | try { |
| 77 | await cache.refresh("cut-race"); |
| 78 | const old = cache.ensure("cut-race", 129); |
| 79 | await Promise.resolve(); await Promise.resolve(); |
| 80 | cut = 13; |
| 81 | await cache.refresh("cut-race"); |
| 82 | finish(page(129)); await old; |
| 83 | await cache.ensure("cut-race", 129); |
| 84 | assert.equal(cache.getView("cut-race").snapshotSequence, 13); |
| 85 | assert.equal(cache.getView("cut-race").mode, "ready", "an obsolete page cannot poison the fresh directory"); |
| 86 | assert.equal(cache.getView("cut-race").entries.get(129)?.messageId, "u129"); |
| 87 | } finally { release(); } |
| 88 | } |
| 89 | { |
| 90 | let finish!: (value: HistoryOutlinePage) => void; |
| 91 | let reads = 0; |
| 92 | const gate = new Promise<HistoryOutlinePage>(resolve => { finish = resolve; }); |
| 93 | const cache = new TranscriptOutlineStore(); |
| 94 | const release = cache.activate("slow", "session", () => { reads++; return gate; }, 25000); |
| 95 | try { |
| 96 | const first = cache.refresh("slow"); |
| 97 | await Promise.resolve(); |
| 98 | const second = cache.refresh("slow"); |
| 99 | await Promise.resolve(); |
| 100 | assert.equal(reads, 1, "slow refresh is shared instead of continually invalidated"); |
| 101 | finish(page()); await Promise.all([first, second]); |
| 102 | assert.equal(cache.getView("slow").mode, "ready"); |
| 103 | } finally { release(); } |
| 104 | } |
| 105 |