返回 DeepSeek-Reasonix
chat-turn-outline-jump.test.tsx
根目录 / desktop / frontend / src / __tests__ / chat-turn-outline-jump.test.tsx
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
115 lines Plain Text