返回 DeepSeek-Reasonix
transcriptFakeBackend.ts
根目录 / desktop / frontend / src / __tests__ / helpers / transcriptFakeBackend.ts
1 import type { HistoryContentChunk, HistoryContentRef, HistoryEntry, HistoryMessage, HistorySlice, HistorySliceRequest } from "../../lib/types";
2 function deferred<T>() {
3 let resolve!: (value: T) => void;
4 let reject!: (reason?: unknown) => void;
5 const promise = new Promise<T>((res, rej) => {
6 resolve = res;
7 reject = rej;
8 });
9 return { promise, resolve, reject };
10 }
11
12 export type RefTable = Map<string, string>; // `${entryId}:${field}` -> full content
13
14 export class FakeBackend {
15 sliceCalls: HistorySliceRequest[] = [];
16 contentCalls: Array<{ ref: HistoryContentRef; chunk: number }> = [];
17 sliceGate: ReturnType<typeof deferred<HistorySlice>> | undefined;
18 contentGate: ReturnType<typeof deferred<HistoryContentChunk>> | undefined;
19 staleNextCursor = false;
20 revision = 1;
21 digest = "digest-1";
22
23 constructor(
24 private readonly messages: HistoryMessage[],
25 private readonly refs: RefTable = new Map(),
26 private readonly sessionId = "s1",
27 ) {}
28
29 private entryId(index: number): string {
30 return `${this.sessionId}:r0:m${index}:o0`;
31 }
32
33 private entriesFor(lo: number, hi: number): HistoryEntry[] {
34 let turn = 0;
35 const turnsOf: number[] = [];
36 for (const message of this.messages) {
37 if (message.role === "user") turn += 1;
38 turnsOf.push(turn);
39 }
40 return this.messages.slice(lo, hi).map((message, offset) => {
41 const index = lo + offset;
42 const entryId = this.entryId(index);
43 const refs: HistoryContentRef[] = [];
44 let msg = message;
45 const full = this.refs.get(`${entryId}:content`);
46 if (full !== undefined) {
47 msg = { ...message, content: full.slice(0, 16) };
48 refs.push({ entryId, field: "content", size: full.length, chunks: 2, revision: 1, digest: "d" });
49 }
50 return { entryId, turn: turnsOf[index], order: index, message: msg, refs };
51 });
52 }
53
54 slice(lo: number, hi: number): HistorySlice {
55 const entries = this.entriesFor(lo, hi);
56 const turns = entries.map((entry) => entry.turn).filter((value) => value > 0);
57 return {
58 entries,
59 nextCursor: lo > 0 ? btoa(JSON.stringify({ v: 1, before: lo })) : "",
60 hasOlder: lo > 0,
61 newerCursor: hi < this.messages.length ? btoa(JSON.stringify({ v: 1, after: hi })) : "",
62 hasNewer: hi < this.messages.length,
63 totalTurns: this.messages.filter((message) => message.role === "user").length,
64 startTurn: turns.length > 0 ? Math.min(...turns) : 0,
65 endTurn: turns.length > 0 ? Math.max(...turns) : 0,
66 stale: false,
67 revision: this.revision,
68 revisionKnown: true,
69 digest: this.digest,
70 };
71 }
72
73 // Turn- and entry-budgeted windowing, mirroring the Go slice semantics for
74 // the compact stress fixtures used below.
75 async HistorySliceForTab(_tabID: string, req: HistorySliceRequest): Promise<HistorySlice> {
76 this.sliceCalls.push(req);
77 if (this.sliceGate) {
78 const gate = this.sliceGate;
79 this.sliceGate = undefined;
80 return gate.promise;
81 }
82 if (req.newer) {
83 // Window paging toward newer history: `after` is an exclusive position.
84 const decoded = JSON.parse(atob(req.cursor)) as { after?: number };
85 const lo = Math.min(this.messages.length, decoded.after ?? this.messages.length);
86 const entries = Math.max(1, Math.floor(req.entries || 120));
87 return this.slice(lo, Math.min(this.messages.length, lo + entries));
88 }
89 let before = this.messages.length;
90 if (req.cursor) {
91 if (this.staleNextCursor) return { entries: [], nextCursor: "", hasOlder: false, totalTurns: 0, startTurn: 0, endTurn: 0, stale: true, revision: this.revision, revisionKnown: true, digest: this.digest };
92 const decoded = JSON.parse(atob(req.cursor)) as { before?: number };
93 before = Math.min(before, decoded.before ?? before);
94 }
95 if (before <= 0 || this.messages.length === 0) return this.slice(0, 0);
96 let turn = 0;
97 const turnsOf: number[] = [];
98 for (const message of this.messages) {
99 if (message.role === "user") turn += 1;
100 turnsOf.push(turn);
101 }
102 const turns = Math.max(1, Math.floor(req.turns || 12));
103 const newestTurn = turnsOf[before - 1];
104 const oldestTurn = newestTurn > 0 ? Math.max(newestTurn - turns + 1, 1) : 0;
105 let lo = 0;
106 if (oldestTurn > 1) {
107 lo = before;
108 for (let i = 0; i < before; i += 1) {
109 if (turnsOf[i] >= oldestTurn) { lo = i; break; }
110 }
111 }
112 const entries = Math.max(1, Math.floor(req.entries || 120));
113 lo = Math.max(lo, before - entries);
114 return this.slice(lo, before);
115 }
116
117 async HistoryContentForTab(_tabID: string, ref: HistoryContentRef, chunkIndex: number): Promise<HistoryContentChunk> {
118 this.contentCalls.push({ ref, chunk: chunkIndex });
119 if (this.contentGate) {
120 const gate = this.contentGate;
121 this.contentGate = undefined;
122 return gate.promise;
123 }
124 const full = this.refs.get(`${ref.entryId}:${ref.field}`) ?? "";
125 const half = Math.ceil(full.length / 2);
126 const data = chunkIndex === 0 ? full.slice(0, half) : full.slice(half);
127 return { entryId: ref.entryId, field: ref.field, chunk: chunkIndex, chunks: 2, data, done: chunkIndex >= 1, stale: false };
128 }
129 }
130
130 lines TYPESCRIPT