| 1 | import assert from "node:assert/strict"; |
| 2 | import { HistoryPreparingError, loadPreparedHistory } from "../lib/historyPreparation"; |
| 3 | import { TranscriptStore } from "../lib/transcriptStore"; |
| 4 | import type { HistorySlice } from "../lib/types"; |
| 5 | import { JSDOM } from "jsdom"; |
| 6 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 7 | import { canonicalHistorySlice } from "../lib/canonicalTranscriptBackend"; |
| 8 | |
| 9 | let calls = 0; |
| 10 | let waits = 0; |
| 11 | const page = await loadPreparedHistory(async () => { |
| 12 | if (++calls < 3) throw new HistoryPreparingError(); |
| 13 | return "ready"; |
| 14 | }, () => true, async () => { waits++; }); |
| 15 | assert.equal(page, "ready"); |
| 16 | assert.equal(waits, 2); |
| 17 | |
| 18 | let current = true; |
| 19 | let cancelledCalls = 0; |
| 20 | const cancelled = await loadPreparedHistory(async () => { |
| 21 | cancelledCalls++; |
| 22 | throw new HistoryPreparingError(); |
| 23 | }, () => current, async () => { current = false; }); |
| 24 | assert.equal(cancelled, undefined); |
| 25 | assert.equal(cancelledCalls, 1, "session changes stop preparation requests"); |
| 26 | |
| 27 | const corruption = new Error("damaged session store"); |
| 28 | await assert.rejects(loadPreparedHistory(async () => { throw corruption; }, () => true, |
| 29 | async () => { assert.fail("real errors must not be retried"); }), error => error === corruption); |
| 30 | |
| 31 | let finish!: (value: string) => void; |
| 32 | current = true; |
| 33 | const pending = loadPreparedHistory(() => new Promise<string>(resolve => { finish = resolve; }), () => current); |
| 34 | current = false; |
| 35 | finish("old session"); |
| 36 | assert.equal(await pending, undefined, "late ready response cannot hydrate another session"); |
| 37 | |
| 38 | const readySlice: HistorySlice = { entries: [{ entryId: "m1", turn: 1, order: 1, message: { role: "user", content: "ready history" }, refs: [] }], nextCursor: "", hasOlder: false, totalTurns: 1, startTurn: 1, endTurn: 1, stale: false, revision: 1 }; |
| 39 | let preparationCalls = 0; |
| 40 | const store = new TranscriptStore({ |
| 41 | HistorySliceForTab: async () => { if (++preparationCalls === 1) throw new HistoryPreparingError(); return readySlice; }, |
| 42 | HistoryContentForTab: async () => { throw new Error("unused"); }, |
| 43 | }, { preparationWait: async () => {} }); |
| 44 | const loaded = await store.loadLatest("tab", "session"); |
| 45 | assert.equal(loaded?.totalTurns, 1, "store completes startup hydration after preparation"); |
| 46 | assert.equal(preparationCalls, 2); |
| 47 | |
| 48 | let resume!: () => void; |
| 49 | let waitStarted!: () => void; |
| 50 | const started = new Promise<void>(resolve => { waitStarted = resolve; }); |
| 51 | let staleCalls = 0; |
| 52 | const changingStore = new TranscriptStore({ |
| 53 | HistorySliceForTab: async () => { if (++staleCalls === 1) throw new HistoryPreparingError(); return readySlice; }, |
| 54 | HistoryContentForTab: async () => { throw new Error("unused"); }, |
| 55 | }, { preparationWait: () => new Promise<void>(resolve => { resume = resolve; waitStarted(); }) }); |
| 56 | const oldLoad = changingStore.loadLatest("tab", "session"); |
| 57 | await started; |
| 58 | const newLoad = await changingStore.loadLatest("tab", "session"); |
| 59 | resume(); |
| 60 | assert.equal(await oldLoad, undefined, "superseded store generation cannot restart its request"); |
| 61 | assert.equal(newLoad?.totalTurns, 1); |
| 62 | assert.equal(staleCalls, 2, "superseded request performs no extra RPC"); |
| 63 | |
| 64 | let pagingCalls = 0; |
| 65 | current = true; |
| 66 | const pagingStore = new TranscriptStore({ |
| 67 | HistorySliceForTab: async (_tab, req) => { |
| 68 | assert.equal("current" in req, false, "lifecycle callback cannot enter the IPC payload"); |
| 69 | if (++pagingCalls > 1) throw new HistoryPreparingError(); |
| 70 | return { ...readySlice, hasOlder: true, nextCursor: "older" }; |
| 71 | }, |
| 72 | HistoryContentForTab: async () => { throw new Error("unused"); }, |
| 73 | }, { preparationWait: async () => { current = false; } }); |
| 74 | await pagingStore.loadLatest("tab", "session"); |
| 75 | assert.equal(await pagingStore.loadOlder("tab", "session", { current: () => current }), undefined); |
| 76 | assert.equal(pagingCalls, 2, "navigation cancels preparation of an older page"); |
| 77 | |
| 78 | const dom = new JSDOM("", { url: "http://localhost/" }); |
| 79 | globalThis.window = dom.window as unknown as Window & typeof globalThis; |
| 80 | let remoteReads = 0; |
| 81 | const host = installDesktopHostStub({ |
| 82 | SessionHistoryWindowForTab: async () => ({ status: "preparing", messages: [] }), |
| 83 | RemoteSessionHistoryWindowForTab: async () => { remoteReads++; throw new Error("wrong host"); }, |
| 84 | }); |
| 85 | await assert.rejects(canonicalHistorySlice("preparing-local", { cursor: "" }), HistoryPreparingError); |
| 86 | assert.equal(remoteReads, 0, "preparing local history must not fall back to remote"); |
| 87 | host.uninstall(); |
| 88 | dom.window.close(); |
| 89 | console.log("history preparation: ready, cancellation, late completion and real errors passed"); |
| 90 |