返回 DeepSeek-Reasonix
chat-turn-jump.test.ts
根目录 / desktop / frontend / src / __tests__ / chat-turn-jump.test.ts
1 import assert from "node:assert/strict";
2 import { ChatMountedOrder } from "../lib/chatMountedOrder";
3 import { ChatTurnJump, type TurnJumpDeps } from "../lib/chatTurnJump";
4 import type { ChatScrollController } from "../lib/chatScrollController";
5 import type { NavigateToTurn, TurnNavigationOutcome } from "../lib/historyTurnNavigation";
6 import { findLoadedTurn, indexLoadedTurns } from "../lib/chatTurnRail";
7
8 function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done; }); return { promise, resolve }; }
9 function setup(navigate: NavigateToTurn, clock?: TurnJumpDeps["clock"]) {
10 const mounts = new ChatMountedOrder(); const mounted = new Set<string>(); const writes: string[] = []; const readers = new Set<() => void>();
11 const scroll = { stopFollowing() {}, jump(key: string) { writes.push(key); }, subscribeReaderIntent(fn: () => void) { readers.add(fn); return () => { readers.delete(fn); }; } } as unknown as ChatScrollController;
12 let refreshes = 0;
13 const jump = new ChatTurnJump({ mounts, scroll, resolveKey: target => mounted.has(target.messageId) ? target.messageId : undefined,
14 navigate, clock, isCurrent: () => true, refresh: async () => { refreshes++; } });
15 return { jump, mounts, mounted, writes, refreshes: () => refreshes, read: () => { for (const fn of [...readers]) fn(); } };
16 }
17 const target = (id: string) => ({ turn: id, resolve: async () => ({ messageId: id, generation: "g", snapshotSequence: 1 }) });
18 {
19 let reads = 0;
20 const h = setup(async t => { reads++; h.mounted.add(t.messageId); return "loaded"; });
21 await h.jump.jump(target("oldest"));
22 assert.equal(reads, 1, "far jump reads one target window");
23 assert.deepEqual(h.writes, ["oldest"]); h.jump.dispose();
24 }
25 {
26 const request = deferred<TurnNavigationOutcome>(); let valid!: () => boolean;
27 const h = setup(async (_target, current) => { valid = current; return request.promise; });
28 const old = h.jump.jump(target("old")); await Promise.resolve();
29 h.mounted.add("new"); h.jump.jumpTo("new");
30 assert.equal(valid(), false); request.resolve("loaded"); await old;
31 assert.deepEqual(h.writes, ["new"]); h.jump.dispose();
32 }
33 {
34 const request = deferred<TurnNavigationOutcome>(); let valid!: () => boolean;
35 const h = setup(async (_target, current) => { valid = current; return request.promise; });
36 const run = h.jump.jump(target("old")); await Promise.resolve(); h.read();
37 assert.equal(valid(), false); request.resolve("loaded"); await run;
38 assert.deepEqual(h.writes, []); h.jump.dispose();
39 }
40 {
41 const identities: string[] = [];
42 const h = setup(async t => { identities.push(t.messageId); if (identities.length === 1) return "stale"; h.mounted.add(t.messageId); return "loaded"; });
43 await h.jump.jump(target("same-message"));
44 assert.deepEqual(identities, ["same-message", "same-message"]); assert.equal(h.refreshes(), 1); h.jump.dispose();
45 }
46 {
47 const h = setup(async () => "stale"); await h.jump.jump(target("gone"));
48 assert.equal(h.refreshes(), 1); assert.equal(h.jump.getSnapshot().reason, "snapshotExpired"); h.jump.dispose();
49 }
50 {
51 const gate = deferred<TurnNavigationOutcome>(); const h = setup(() => gate.promise);
52 const run = h.jump.jump(target("later-mount")); await Promise.resolve(); gate.resolve("loaded"); await Promise.resolve(); await Promise.resolve();
53 assert.deepEqual(h.writes, []); h.mounted.add("later-mount"); h.mounts.publish(["later-mount"]); await run;
54 assert.deepEqual(h.writes, ["later-mount"]); h.jump.dispose();
55 }
56 const loaded = indexLoadedTurns(["optimistic"], () => ({ id: "optimistic", messageId: "stable" }));
57 {
58 let timeout!: () => void;
59 const h = setup(async () => "loaded", {
60 setTimeout: ((fn: () => void) => { timeout = fn; return 1; }) as unknown as typeof setTimeout,
61 clearTimeout: (() => {}) as typeof clearTimeout,
62 });
63 const run = h.jump.jump(target("never-mounted"));
64 await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
65 timeout(); await run;
66 assert.equal(h.jump.getSnapshot().reason, "mountTimeout");
67 assert.deepEqual(h.writes, []); h.jump.dispose();
68 }
69 {
70 const selected: string[] = [];
71 const h = setup(async t => { selected.push(t.messageId); return "unavailable"; });
72 let id = "original";
73 await h.jump.jump({ turn: "pending:1", resolve: async () => ({ messageId: id }) });
74 id = "replacement";
75 await h.jump.retry();
76 assert.deepEqual(selected, ["original", "original"], "retry never resolves an ordinal to a different message");
77 h.jump.dispose();
78 }
79 assert.equal(findLoadedTurn(loaded, { id: "m:stable", messageId: "stable", turn: 1, order: 0, prompt: "" }), "optimistic");
80 console.log("turn jump: direct read, identity, latest intent, native takeover and mount confirmation passed");
81
81 lines TYPESCRIPT