| 1 | import assert from "node:assert/strict"; |
| 2 | import { act } from "react"; |
| 3 | import { createTranscriptHarness } from "./transcript-dom-harness"; |
| 4 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 5 | import type { FollowRequest, TranscriptFollowResponse, HistoryOutlineRequest, HistoryWindowRequest } from "../generated/desktopContract.generated"; |
| 6 | import type * as Controller from "../lib/useController"; |
| 7 | import type * as Follower from "../lib/transcriptSessionFollower"; |
| 8 | import type * as Navigation from "../lib/historyTurnNavigation"; |
| 9 | |
| 10 | for (const remote of [false, true]) { |
| 11 | const harness = await createTranscriptHarness({ deterministic: true, viewportHeight: 300, rowHeight: 80 }); |
| 12 | const tab = remote ? "remote-outline" : "local-outline"; |
| 13 | const prefix = remote ? "Remote" : ""; |
| 14 | const outlineRequests: HistoryOutlineRequest[] = []; |
| 15 | const windows: HistoryWindowRequest[] = []; |
| 16 | let block: ((request: HistoryWindowRequest) => Promise<void>) | undefined; |
| 17 | const message = (turn: number) => ({ messageId: `u${turn}`, position: turn, version: 1, role: "user", eventSequence: turn, |
| 18 | visibleTurn: turn, preview: `question ${turn}`, inline: { id: `u${turn}`, role: "user", content: `question ${turn}` } }); |
| 19 | const baseline: TranscriptFollowResponse = { |
| 20 | protocolVersion: 2, subscription: tab, changes: [], resetRequired: false, |
| 21 | snapshot: { protocolVersion: 1, snapshotId: "tail", identity: { sessionId: tab, runtimeEpoch: "epoch", rewriteEpoch: 0, headId: "" }, |
| 22 | projectionRevision: 10, coveredThroughSeq: 6, durableSeq: 6, records: [], activeRecords: [], activeAttempts: [], |
| 23 | runtime: { status: "completed", pendingEvents: [], samplingCount: 0, toolCount: 0 }, before: 0, hasOlder: true, totalRecords: 1, totalTurns: 6, stale: false }, |
| 24 | history: { status: "ready", snapshotSequence: 6, coverageSequence: 6, generation: "durable", totalTurns: 6, |
| 25 | hasOlder: true, hasNewer: false, olderCursor: "older", messages: [message(6)] }, |
| 26 | }; |
| 27 | const stub = installDesktopHostStub({ |
| 28 | [`${prefix}TranscriptFollowForTab`]: (_tab: string, request: FollowRequest) => request.close |
| 29 | ? Promise.resolve({ protocolVersion: 2, changes: [], resetRequired: false, subscription: tab }) |
| 30 | : request.subscription ? new Promise(() => {}) : Promise.resolve(baseline), |
| 31 | [`${prefix}SessionHistoryOutlineForTab`]: async (_tab: string, request: HistoryOutlineRequest) => { |
| 32 | outlineRequests.push(request); |
| 33 | return { status: "ready", generation: "durable", snapshotSequence: 6, coverageSequence: 6, totalTurns: 6, done: true, nextTurn: 7, |
| 34 | entries: Array.from({ length: 6 }, (_, i) => ({ messageId: `u${i + 1}`, turn: i + 1, position: i + 1, prompt: `question ${i + 1}`, answer: `answer ${i + 1}` })) }; |
| 35 | }, |
| 36 | [`${prefix}SessionHistoryWindowForTab`]: async (_tab: string, request: HistoryWindowRequest) => { |
| 37 | windows.push(request); |
| 38 | await block?.(request); |
| 39 | const turn = request.messageId ? Number(request.messageId.slice(1)) : 5; |
| 40 | return { status: "ready", snapshotSequence: 6, coverageSequence: 6, generation: "durable", totalTurns: 6, |
| 41 | hasOlder: turn > 1, hasNewer: turn < 6, olderCursor: turn > 1 ? "older" : "", newerCursor: turn < 6 ? "newer" : "", messages: [message(turn)] }; |
| 42 | }, |
| 43 | }); |
| 44 | const controller = await harness.loadModule<typeof Controller>("/src/lib/useController.ts"); |
| 45 | const { TranscriptSessionFollower } = await harness.loadModule<typeof Follower>("/src/lib/transcriptSessionFollower.ts"); |
| 46 | const { navigateHistoryTurn } = await harness.loadModule<typeof Navigation>("/src/lib/historyTurnNavigation.ts"); |
| 47 | const backend = await harness.loadModule<typeof import("../lib/canonicalTranscriptBackend")>("/src/lib/canonicalTranscriptBackend.ts"); |
| 48 | backend.setTranscriptBindingIdentity(() => remote ? "remote" : "local"); |
| 49 | let state = { ...controller.initialState, meta: { sessionPath: tab } } as Controller.State; |
| 50 | const dispatch = (action: Controller.Action) => { state = controller.reducer(state, action); }; |
| 51 | const follower = new TranscriptSessionFollower(tab, tab, remote, dispatch, () => state); |
| 52 | const render = () => harness.render(state.items, { tabId: tab, hostId: remote ? "host" : "local", geometrySessionKey: tab, |
| 53 | totalTurns: state.historyTotalTurns, historyStartTurn: state.historyStartTurn, hasOlderHistory: state.historyHasOlder, hasNewerHistory: state.historyHasNewer, |
| 54 | onNavigateToTurn: (target: Navigation.TurnNavigationTarget, current: () => boolean) => navigateHistoryTurn(tab, state, () => state, dispatch, target, current) }); |
| 55 | try { |
| 56 | await follower.start(); await render(); await harness.settle(); |
| 57 | await harness.waitFor(() => harness.container.querySelectorAll('[data-nav-turn="m:u1"]').length === 1, "production directory initialization"); |
| 58 | assert.equal(harness.container.querySelectorAll("[data-nav-turn]").length, 6, "one body turn still exposes the full rail"); |
| 59 | assert.equal(state.items.filter(item => item.kind === "user").length, 1); |
| 60 | for (const turn of [1, 3, 6]) { |
| 61 | await act(async () => { harness.container.querySelector<HTMLElement>(`[data-nav-turn="m:u${turn}"]`)!.click(); }); |
| 62 | await harness.waitFor(() => state.items.some(item => item.kind === "user" && item.messageId === `u${turn}`), "target window publication"); |
| 63 | await render(); await harness.settle(); |
| 64 | assert.ok(harness.container.querySelector(`[data-chat-anchor-key="m:u${turn}"]`), `turn ${turn} mounted`); |
| 65 | } |
| 66 | assert.equal(windows.length, 3, "each far jump requests one window, never intervening pages"); |
| 67 | assert.ok(windows.every(request => request.generation === "durable" && request.snapshotSequence === 6)); |
| 68 | assert.ok(outlineRequests.length <= 2, "body navigation does not refetch the whole directory"); |
| 69 | const { getTranscriptStore } = await harness.loadModule<typeof import("../lib/transcriptStore")>("/src/lib/transcriptStore.ts"); |
| 70 | const store = getTranscriptStore(); |
| 71 | let resume!: () => void; |
| 72 | const gate = new Promise<void>(resolve => { resume = resolve; }); |
| 73 | block = request => request.cursor ? gate : Promise.resolve(); |
| 74 | const oldPage = store.loadOlder(tab, tab); |
| 75 | await harness.waitFor(() => windows.length === 4, "older read started"); |
| 76 | const replacement = await store.prepareTargetWindow(tab, tab, { anchor: "message", messageId: "u3", direction: "newer", limit: 32 }, () => true); |
| 77 | assert.equal(replacement.status, "loaded"); |
| 78 | if (replacement.status === "loaded") { |
| 79 | assert.equal(replacement.current(), true); |
| 80 | replacement.prepared.commit(); |
| 81 | } |
| 82 | resume(); |
| 83 | assert.equal(await oldPage, undefined, "late ordinary paging cannot overwrite a target window"); |
| 84 | const targetGate = new Promise<void>(resolve => { resume = resolve; }); |
| 85 | block = request => request.messageId ? targetGate : Promise.resolve(); |
| 86 | const superseded = store.prepareTargetWindow(tab, tab, { anchor: "message", messageId: "u1", direction: "newer", limit: 32 }, () => true); |
| 87 | await harness.waitFor(() => windows.length === 6, "target read started"); |
| 88 | await store.loadNewer(tab, tab); |
| 89 | resume(); |
| 90 | assert.equal((await superseded).status, "cancelled", "newer paging supersedes a pending target"); |
| 91 | const latestGate = new Promise<void>(resolve => { resume = resolve; }); |
| 92 | block = request => request.anchor === "newest" ? latestGate : Promise.resolve(); |
| 93 | const pendingLatest = store.loadLatest(tab, tab); |
| 94 | await harness.waitFor(() => windows.length === 8, "latest read started"); |
| 95 | const preparedTarget = await store.prepareTargetWindow(tab, tab, { anchor: "message", messageId: "u1", direction: "newer", limit: 32 }, () => true); |
| 96 | assert.equal(preparedTarget.status, "loaded"); |
| 97 | resume(); |
| 98 | assert.equal(await pendingLatest, undefined, "older latest read is obsolete before target publication"); |
| 99 | const { loadHistoryWindow } = await harness.loadModule<typeof import("../lib/historyWindowController")>("/src/lib/historyWindowController.ts"); |
| 100 | let readerCurrent = true; |
| 101 | const cancelGate = new Promise<void>(resolve => { resume = resolve; }); |
| 102 | block = () => cancelGate; |
| 103 | const cancelledLatest = loadHistoryWindow({ tabId: tab, direction: "latest", trigger: "return-latest", state, |
| 104 | requestSeq: 1, isCurrent: () => true, readerCurrent: () => readerCurrent, currentState: () => state, dispatch }); |
| 105 | await harness.waitFor(() => windows.length === 10, "reader-cancellable latest read started"); |
| 106 | const beforeCancellation = state.items; |
| 107 | readerCurrent = false; resume(); |
| 108 | assert.equal(await cancelledLatest, "empty"); |
| 109 | assert.equal(state.items, beforeCancellation, "reader cancellation preserves the visible body"); |
| 110 | assert.equal(state.historyNewerLoading, false); |
| 111 | assert.equal(state.historyNewerError, "", "intent cancellation is not a network failure"); |
| 112 | } finally { follower.stop(); await harness.close(); stub.uninstall(); } |
| 113 | } |
| 114 | console.log("production Follow v2: local and remote full directory, one-turn tail and direct jumps passed"); |
| 115 |