返回 DeepSeek-Reasonix
native-transcript-history.test.ts
根目录 / desktop / frontend / src / __tests__ / native-transcript-history.test.ts
1 import assert from "node:assert/strict";
2 import test from "node:test";
3 import { installDesktopHostStub } from "./desktopHostStub";
4 import type { TranscriptSnapshot } from "../lib/transcriptProtocol";
5
6 Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
7 const commands: Record<string, unknown> = {};
8 installDesktopHostStub(commands);
9 const { bindNativeTranscriptHistory, nativeSnapshotWindow, readNativeTranscriptWindow, readNativeTranscriptOutline } = await import("../lib/nativeTranscriptHistory");
10
11 function snapshot(before: number, count: number, total = 100): TranscriptSnapshot {
12 const start = Math.max(0, before - count);
13 return { protocolVersion: 1, snapshotId: "cut", identity: { sessionId: "old", headId: "main", rewriteEpoch: 0, runtimeEpoch: "epoch" },
14 coveredThroughSeq: 7, projectionRevision: 7, runtime: { pendingEvents: [] }, activeRecords: [], activeAttempts: [],
15 records: Array.from({ length: before - start }, (_, index) => ({ id: `m:${start + index}`, order: start + index,
16 message: { role: "user", messageId: String(start + index), content: "text", historyTurn: start + index + 1 }, refs: [] })),
17 before: start, hasOlder: start > 0, totalRecords: total, totalTurns: total, stale: false };
18 }
19
20 test("native pages remain bounded and reachable in both directions", async () => {
21 commands.TranscriptPageForTab = async (_: string, req: { before: number; records: number }) => snapshot(Math.min(100, req.before), req.records);
22 const initial = snapshot(100, 32);
23 const release = bindNativeTranscriptHistory("native", initial, false);
24 let page = nativeSnapshotWindow(initial);
25 const seen = new Set(page.entries.map(entry => entry.entryId));
26 while (page.hasOlder) {
27 page = (await readNativeTranscriptWindow("native", { anchor: "cursor", cursor: page.olderCursor, limit: 32 }))!;
28 assert.ok(page.entries.length <= 32);
29 page.entries.forEach(entry => seen.add(entry.entryId));
30 }
31 assert.equal(seen.size, 100);
32 let nextOrder = page.entries[page.entries.length - 1]!.order + 1;
33 while (page.hasNewer) {
34 page = (await readNativeTranscriptWindow("native", { anchor: "cursor", cursor: page.newerCursor, limit: 32 }))!;
35 assert.ok(page.entries.length <= 32);
36 assert.equal(page.entries[0].order, nextOrder, "forward pages neither overlap nor skip");
37 nextOrder = page.entries[page.entries.length - 1]!.order + 1;
38 }
39 assert.equal(page.entries[page.entries.length - 1]?.entryId, "m:99");
40 release();
41 });
42
43 test("a requested fixed cut is retained and conflicting cursors are stale", async () => {
44 commands.TranscriptPageForTab = async (_: string, req: { snapshotId: string; before: number; records: number }) => {
45 assert.equal(req.snapshotId, "previous-cut");
46 assert.equal(req.before, undefined, "newest uses the requested cut's end, not the live binding's size");
47 return { ...snapshot(150, req.records, 150), snapshotId: req.snapshotId };
48 };
49 const release = bindNativeTranscriptHistory("fixed", snapshot(100, 32), false);
50 const fixed = await readNativeTranscriptWindow("fixed", { generation: "previous-cut", anchor: "newest" });
51 assert.equal(fixed?.digest, "previous-cut");
52 assert.equal(fixed?.entries[fixed.entries.length - 1]?.order, 149);
53 const old = nativeSnapshotWindow(snapshot(100, 32));
54 assert.equal((await readNativeTranscriptWindow("fixed", { anchor: "cursor", generation: "previous-cut", cursor: old.olderCursor }))?.status, "stale_cursor");
55 assert.equal((await readNativeTranscriptWindow("fixed", { anchor: "cursor", cursor: "broken" }))?.status, "stale_cursor");
56 release();
57 });
58
59 test("late native pages cannot publish after replacement and never fall back to another host", async () => {
60 let finish!: (value: TranscriptSnapshot) => void;
61 commands.TranscriptPageForTab = () => new Promise<TranscriptSnapshot>(resolve => { finish = resolve; });
62 commands.RemoteTranscriptPageForTab = () => { throw new Error("wrong host"); };
63 const releaseOld = bindNativeTranscriptHistory("switched", snapshot(100, 32), false);
64 const pending = readNativeTranscriptWindow("switched", { anchor: "newest", limit: 32 });
65 const releaseNew = bindNativeTranscriptHistory("switched", snapshot(100, 32), false);
66 releaseOld();
67 finish(snapshot(100, 32));
68 assert.equal((await pending)?.status, "stale_cursor");
69 commands.TranscriptPageForTab = () => { throw new Error("disk error"); };
70 await assert.rejects(readNativeTranscriptWindow("switched", { anchor: "newest" }), /disk error/);
71 releaseNew();
72 assert.equal(await readNativeTranscriptWindow("switched", { anchor: "newest" }), undefined);
73 });
74
75 test("a byte-limited forward page does not skip oversized records", async () => {
76 commands.TranscriptPageForTab = async (_: string, req: { before: number }) => snapshot(req.before, 1);
77 const release = bindNativeTranscriptHistory("large", snapshot(100, 32), false);
78 const older = nativeSnapshotWindow(snapshot(32, 32));
79 const next = await readNativeTranscriptWindow("large", { anchor: "cursor", cursor: older.newerCursor, limit: 32 });
80 assert.equal(next?.entries[0].order, 32);
81 release();
82 });
83
84 test("late missing-turn lookup cannot publish into a replacement binding", async () => {
85 let finish!: (value: unknown) => void;
86 commands.TranscriptOutlineForTab = () => new Promise(resolve => { finish = resolve; });
87 commands.TranscriptPageForTab = () => { throw new Error("retired lookup continued"); };
88 const releaseOld = bindNativeTranscriptHistory("turn-switch", snapshot(100, 32), false);
89 const pending = readNativeTranscriptWindow("turn-switch", { anchor: "turn", turn: 1000 });
90 const releaseNew = bindNativeTranscriptHistory("turn-switch", snapshot(100, 32), false);
91 releaseOld();
92 finish({ entries: [], stale: false });
93 assert.equal((await pending)?.status, "stale_cursor");
94 releaseNew();
95 });
96
97 test("remote native message and outline lookup use the negotiated host and fixed cut", async () => {
98 commands.TranscriptPageForTab = () => { throw new Error("wrong host"); };
99 commands.TranscriptOutlineForTab = () => { throw new Error("wrong host"); };
100 commands.RemoteTranscriptPageForTab = async (_: string, req: { snapshotId: string; messageId: string }) => {
101 assert.equal(req.snapshotId, "cut");
102 assert.equal(req.messageId, "50");
103 return snapshot(51, 1);
104 };
105 commands.RemoteTranscriptOutlineForTab = async (_: string, req: { snapshotId: string; offset: number }) => {
106 assert.equal(req.snapshotId, "cut");
107 assert.equal(req.offset, 50);
108 return { entries: [{ id: "m:50", messageId: "50", turn: 51, order: 50, prompt: "question", answer: "answer" }], total: 100, nextOffset: 51, done: false, stale: false };
109 };
110 const release = bindNativeTranscriptHistory("remote-native", snapshot(100, 32), true);
111 const message = await readNativeTranscriptWindow("remote-native", { anchor: "message", messageId: "50", generation: "cut" });
112 assert.equal(message?.entries[0].entryId, "m:50");
113 const outline = await readNativeTranscriptOutline("remote-native", { generation: "cut", startTurn: 51, limit: 1 });
114 assert.equal(outline?.entries[0].messageId, "50");
115 assert.equal(outline?.nextTurn, 52);
116 release();
117 });
118
118 lines TYPESCRIPT