返回 DeepSeek-Reasonix
history-read-binding.test.ts
根目录 / desktop / frontend / src / __tests__ / history-read-binding.test.ts
1 import assert from "node:assert/strict";
2 import test from "node:test";
3 import { installDesktopHostStub } from "./desktopHostStub";
4
5 Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
6 const commands: Record<string, unknown> = {};
7 installDesktopHostStub(commands);
8 const { readBoundHistoryWindow, searchBoundHistory, releaseHistoryRead } = await import("../lib/historyReadBinding");
9 const { TranscriptStore } = await import("../lib/transcriptStore");
10 const { FakeBackend } = await import("./helpers/transcriptFakeBackend");
11 function deferred<T>() {
12 let resolve!: (value: T) => void;
13 const promise = new Promise<T>(done => { resolve = done; });
14 return { promise, resolve };
15 }
16 const handle = (id: string) => ({ id, storageBackend: "canonical", sessionGeneration: 1, capabilities: ["history-read-binding-v1"] });
17 const page = { status: "ready", messages: [], snapshotSequence: 1, coverageSequence: 1, generation: "g", totalTurns: 0, hasOlder: false, hasNewer: false };
18
19 test("late Begin is released without touching replacement navigation", async () => {
20 const first = deferred<ReturnType<typeof handle>>();
21 const began = deferred<void>();
22 const released: string[] = [];
23 let begins = 0;
24 commands.BeginSessionHistoryReadForTab = () => {
25 begins++;
26 if (begins === 1) { began.resolve(); return first.promise; }
27 return handle("new");
28 };
29 commands.ReleaseSessionHistoryRead = (id: string) => { released.push(id); };
30 commands.ReadSessionHistoryWindow = () => page;
31 const old = readBoundHistoryWindow("A", { anchor: "newest", limit: 32 });
32 await began.promise;
33 releaseHistoryRead("A");
34 assert.equal((await readBoundHistoryWindow("A", { anchor: "newest", limit: 32 }))?.status, "ready");
35 first.resolve(handle("old"));
36 assert.equal((await old)?.status, "stale_cursor");
37 assert.deepEqual(released, ["old"]);
38 releaseHistoryRead("A");
39 await Promise.resolve();
40 assert.deepEqual(released, ["old", "new"]);
41 });
42
43 test("late page cannot publish into A after A to B to A", async () => {
44 const reply = deferred<typeof page>();
45 const entered = deferred<void>();
46 let reads = 0, begins = 0;
47 commands.BeginSessionHistoryReadForTab = () => handle(String(++begins));
48 commands.ReadSessionHistoryWindow = () => { if (++reads === 1) { entered.resolve(); return reply.promise; } return page; };
49 const old = readBoundHistoryWindow("A", { anchor: "newest", limit: 32 });
50 await entered.promise;
51 releaseHistoryRead("A");
52 await readBoundHistoryWindow("B", { anchor: "newest", limit: 32 });
53 releaseHistoryRead("B");
54 assert.equal((await readBoundHistoryWindow("A", { anchor: "newest", limit: 32 }))?.status, "ready");
55 reply.resolve(page);
56 assert.equal((await old)?.status, "stale_cursor");
57 releaseHistoryRead("A");
58 });
59
60 test("a replaced source retires the stale preparation before a fresh window", async () => {
61 let begins = 0;
62 const released: string[] = [];
63 commands.BeginSessionHistoryReadForTab = () => handle(String(++begins));
64 commands.ReleaseSessionHistoryRead = (id: string) => { released.push(id); };
65 commands.ReadSessionHistoryWindow = (id: string) => ({ ...page, status: id === "1" ? "stale_cursor" : "ready" });
66 assert.equal((await readBoundHistoryWindow("changed", { anchor: "newest" }))?.status, "stale_cursor");
67 assert.equal((await readBoundHistoryWindow("changed", { anchor: "newest" }))?.status, "ready");
68 assert.equal(begins, 2, "the new read cannot reuse a failed source generation forever");
69 assert.deepEqual(released, ["1"]);
70 releaseHistoryRead("changed");
71 });
72
73 test("native anchors carry the outline cut only after capability negotiation", async () => {
74 let navigation = false;
75 const requests: Record<string, unknown>[] = [];
76 commands.BeginSessionHistoryReadForTab = () => ({ ...handle("native"), storageBackend: "legacy",
77 capabilities: ["history-read-binding-v1", ...(navigation ? ["history-native-navigation-v1"] : [])] });
78 commands.ReadSessionHistorySlice = (_id: string, req: Record<string, unknown>) => {
79 requests.push(req);
80 return { status: "ready", page: { entries: [], hasOlder: false, hasNewer: true, totalTurns: 200, startTurn: 17, endTurn: 17, revision: 9, revisionKnown: true, digest: "proof" } };
81 };
82 assert.equal((await readBoundHistoryWindow("native", { anchor: "turn", turn: 17 }))?.status, "unsupported");
83 assert.equal(requests.length, 0);
84 releaseHistoryRead("native");
85 navigation = true;
86 const result = await readBoundHistoryWindow("native", { anchor: "turn", turn: 17, generation: "bound-cut", snapshotSequence: 9, limit: 32 });
87 assert.equal(result?.status, "ready");
88 assert.equal(requests[requests.length - 1]?.anchor, "turn");
89 assert.equal(requests[requests.length - 1]?.turn, 17);
90 assert.equal(requests[requests.length - 1]?.generation, "bound-cut");
91 assert.equal(requests[requests.length - 1]?.snapshotSequence, 9);
92 await readBoundHistoryWindow("native", { anchor: "message", messageId: "sone:r9:m32:o0", generation: "bound-cut" });
93 assert.equal(requests[requests.length - 1]?.messageId, "sone:r9:m32:o0");
94 releaseHistoryRead("native");
95 });
96
97 test("bound search negotiates native support and fences a late reply after navigation", async () => {
98 let enabled = false, reads = 0;
99 commands.BeginSessionHistoryReadForTab = () => ({ ...handle("search"), storageBackend: "legacy",
100 capabilities: ["history-read-binding-v1", ...(enabled ? ["history-native-search-v1"] : [])] });
101 const result = { hits: [{ messageId: "old", preview: "old", position: 1, role: "user", eventSequence: 1 }],
102 status: "ready", snapshotSequence: 1, coverageSequence: 1, hasMore: false };
103 const reply = deferred<typeof result>(), entered = deferred<void>();
104 commands.SearchSessionHistoryRead = () => { reads++; entered.resolve(); return reply.promise; };
105 assert.equal((await searchBoundHistory("search", "needle"))?.status, "unsupported");
106 assert.equal(reads, 0);
107 releaseHistoryRead("search");
108 enabled = true;
109 const old = searchBoundHistory("search", "needle");
110 await entered.promise;
111 releaseHistoryRead("search");
112 commands.SearchSessionHistoryRead = () => ({ ...result, hits: [], status: "preparing", coverageSequence: 0 });
113 assert.equal((await searchBoundHistory("search", "needle"))?.status, "preparing");
114 reply.resolve(result);
115 const late = await old;
116 assert.equal(late?.status, "stale_cursor");
117 assert.deepEqual(late?.hits, []);
118 commands.SearchSessionHistoryRead = () => ({ ...result, hits: [], status: "stale_cursor" });
119 assert.equal((await searchBoundHistory("search", "needle"))?.status, "stale_cursor");
120 releaseHistoryRead("search");
121 });
122
123 test("cache eviction releases its pending read without releasing a rebound tab", async () => {
124 const released: string[] = [];
125 const first = deferred<ReturnType<typeof handle>>(), began = deferred<void>();
126 let begins = 0;
127 commands.BeginSessionHistoryReadForTab = () => {
128 if (++begins === 1) { began.resolve(); return first.promise; }
129 return handle(`read-${begins}`);
130 };
131 commands.ReleaseSessionHistoryRead = (id: string) => { released.push(id); };
132 commands.ReadSessionHistoryWindow = () => page;
133 const backend = new FakeBackend([{ role: "user", content: "history" }]);
134 const store = new TranscriptStore(backend, { maxResidentSessions: 1 });
135 store.noteSessionBinding("evicted", "/old", "s\0old");
136 await store.loadLatest("evicted", "/old");
137 const oldRead = readBoundHistoryWindow("evicted", { anchor: "newest" });
138 await began.promise;
139 store.noteSessionBinding("other", "/other", "s\0other");
140 await store.loadLatest("other", "/other");
141 assert.equal(store.isResident("evicted", "/old"), false);
142 first.resolve(handle("late-evicted"));
143 assert.equal((await oldRead)?.status, "stale_cursor");
144 assert.deepEqual(released, ["late-evicted"], "eviction releases even a late Begin response");
145
146 store.noteSessionBinding("other", "/replacement", "s\0replacement");
147 assert.equal((await readBoundHistoryWindow("other", { anchor: "newest" }))?.status, "ready");
148 await store.loadLatest("other", "/replacement");
149 assert.equal(store.isResident("other", "/other"), false);
150 assert.equal((await readBoundHistoryWindow("other", { anchor: "newest" }))?.status, "ready");
151 assert.equal(begins, 2, "evicting the old cached source must retain the replacement binding");
152 assert.deepEqual(released, ["late-evicted"]);
153 releaseHistoryRead("other");
154 await Promise.resolve();
155 assert.deepEqual(released, ["late-evicted", "read-2"]);
156 });
157
157 lines TYPESCRIPT