返回 DeepSeek-Reasonix
checkpoint-turn-event.test.ts
根目录 / desktop / frontend / src / __tests__ / checkpoint-turn-event.test.ts
1 // Run: tsx src/__tests__/checkpoint-turn-event.test.ts
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6 import { createTurnSubmissionId, initialState, reducer } from "../lib/useController";
7
8 type ReducerState = ReturnType<typeof reducer>;
9
10 let passed = 0;
11 let failed = 0;
12
13 function eq(actual: unknown, expected: unknown, label: string) {
14 if (actual === expected) {
15 process.stdout.write(` PASS ${label}\n`);
16 passed += 1;
17 } else {
18 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`);
19 failed += 1;
20 }
21 }
22
23 function submit(state: ReducerState, text: string, seq: number): ReducerState {
24 return reducer(state, { type: "user", text, seq, submissionId: `s${seq}` });
25 }
26
27 function userById(state: ReducerState, id: string) {
28 const item = state.items.find((candidate) => candidate.kind === "user" && candidate.id === id);
29 if (item?.kind === "user") return item;
30 const local = Object.values(state.localSubmissions).find((candidate) => candidate.localId === id);
31 return local ? {
32 kind: "user" as const,
33 id: local.localId,
34 submissionId: local.submissionId,
35 text: local.text,
36 checkpointTurn: local.checkpointTurn,
37 failed: local.status === "failed",
38 } : undefined;
39 }
40
41 function localById(state: ReducerState, id: string) {
42 return Object.values(state.localSubmissions).find((candidate) => candidate.localId === id);
43 }
44
45 function checkpoint(state: ReducerState, id: string): number | undefined {
46 return userById(state, id)?.checkpointTurn;
47 }
48
49 console.log("\nturn checkpoint submission binding");
50
51 {
52 const base = createTurnSubmissionId("tab-a", 0, 0, "runtime-a");
53 eq(createTurnSubmissionId("tab-b", 0, 0, "runtime-a") === base, false, "submission correlation is tab-scoped");
54 eq(createTurnSubmissionId("tab-a", 1, 0, "runtime-a") === base, false, "submission correlation changes across session reset");
55 eq(createTurnSubmissionId("tab-a", 0, 0, "runtime-b") === base, false, "submission correlation changes across runtime epoch");
56 eq(createTurnSubmissionId("tab-a", 0, 1, "runtime-a") === base, false, "submission correlation changes across local sequence");
57 }
58
59 {
60 let state = submit(initialState, "turn zero", 0);
61 eq(userById(state, "u0")?.submissionId, "s0", "render item id and opaque submission correlation stay distinct");
62 state = reducer(state, { type: "event", e: { kind: "turn_started" } });
63 state = reducer(state, { type: "event", e: { kind: "notice", level: "info", text: "runtime notice" } });
64 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s0", checkpointTurn: 0 } });
65
66 eq(checkpoint(state, "u0"), 0, "turn zero is assigned by exact submission id");
67 eq(localById(state, "u0")?.settled, true, "TurnDone consumes the local checkpoint authority once");
68 eq(state.pendingSubmissionId, undefined, "matching TurnDone confirms the optimistic submit");
69 eq(state.items.some((item) => item.kind === "notice" && item.text === "runtime notice"), true, "intervening items cannot change the target");
70 }
71
72 {
73 let state = submit(initialState, "legacy turn", 1);
74 state = reducer(state, { type: "event", e: { kind: "turn_done", checkpointTurn: 9 } });
75 eq(checkpoint(state, "u1"), undefined, "TurnDone without submission id cannot stamp a local user");
76 eq(state.pendingSubmissionId, "s1", "id-less TurnDone cannot confirm a pending submission");
77
78 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "missing", checkpointTurn: 10 } });
79 eq(checkpoint(state, "u1"), undefined, "unknown submission id cannot rewrite another user");
80 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s1" } });
81 eq(localById(state, "u1")?.settled, true, "matching checkpoint-less TurnDone still settles the correlation");
82 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s1", checkpointTurn: 11 } });
83 eq(checkpoint(state, "u1"), undefined, "duplicate TurnDone cannot backfill a settled checkpoint-less turn");
84 }
85
86 {
87 let state = submit(initialState, "/effort max", 10);
88 state = reducer(state, { type: "send_confirmed", submissionId: "s10" });
89 eq(state.pendingSubmissionId, undefined, "successful local command Promise confirms its exact correlation");
90 state = reducer(state, { type: "controller_rebuilt" });
91
92 state = submit(state, "normal after effort", 11);
93 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s11", checkpointTurn: 1 } });
94 eq(checkpoint(state, "u10"), undefined, "management command stays non-rewindable");
95 eq(checkpoint(state, "u11"), 1, "the next normal turn maps independently after a no-TurnDone command");
96 }
97
98 {
99 const current = reducer(initialState, {
100 type: "history_replace",
101 items: [{ kind: "user", id: "rev-12", text: "new" }],
102 startTurn: 0,
103 totalTurns: 1,
104 hasOlder: false,
105 revision: 12,
106 digest: "digest-12",
107 });
108 const stale = reducer(current, {
109 type: "history_replace",
110 items: [{ kind: "user", id: "rev-11", text: "stale" }],
111 startTurn: 0,
112 totalTurns: 1,
113 hasOlder: false,
114 revision: 11,
115 digest: "digest-11",
116 });
117 eq(stale, current, "an older history revision cannot replace the current transcript projection");
118 }
119
120 {
121 let state = submit(initialState, "activity A", 15);
122 state = reducer(state, { type: "unsend" });
123 state = submit(state, "activity B", 16);
124 state = reducer(state, { type: "event", e: { kind: "turn_started", submissionId: "s15" } });
125 eq(state.pendingSubmissionId, "s16", "old-id runtime activity cannot confirm the new pending user");
126 state = reducer(state, { type: "event", e: { kind: "notice", submissionId: "s16", level: "info", text: "admitted" } });
127 eq(state.pendingSubmissionId, undefined, "matching non-TurnDone activity confirms the pending user");
128 }
129
130 {
131 let state = submit(initialState, "extension activity", 17);
132 const extension = { pluginId: "demo", surfaceId: "status", kind: "status" as const, status: { label: "working" } };
133 state = reducer(state, { type: "event", e: { kind: "extension_status", extension } });
134 eq(state.pendingSubmissionId, "s17", "uncorrelated extension activity cannot confirm a pending user");
135 state = reducer(state, { type: "event", e: { kind: "extension_status", submissionId: "s17", extension } });
136 eq(state.pendingSubmissionId, undefined, "exactly correlated extension activity confirms the pending user");
137 }
138
139 {
140 let state = submit(initialState, "lost A", 20);
141 state = reducer(state, { type: "unsend" });
142 state = submit(state, "B after lost A", 21);
143 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s21", checkpointTurn: 2 } });
144
145 eq(checkpoint(state, "u20"), undefined, "a missing A TurnDone does not consume B ownership");
146 eq(checkpoint(state, "u21"), 2, "B maps by id even when A TurnDone never arrives");
147 }
148
149 {
150 let state = submit(initialState, "cancelled A", 30);
151 state = reducer(state, { type: "unsend" });
152 state = submit(state, "pending B", 31);
153 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s30", checkpointTurn: 3 } });
154
155 eq(checkpoint(state, "u30"), 3, "late A TurnDone still maps exact A");
156 eq(checkpoint(state, "u31"), undefined, "late A TurnDone cannot stamp B");
157 eq(state.pendingSubmissionId, "s31", "late A TurnDone cannot confirm B pending state");
158 eq(state.pendingUser, "pending B", "late A TurnDone cannot flush B pending text");
159
160 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s31", checkpointTurn: 4 } });
161 eq(checkpoint(state, "u31"), 4, "B later receives its own checkpoint");
162 }
163
164 {
165 let state = submit(initialState, "A before failed B", 40);
166 state = reducer(state, { type: "unsend" });
167 state = submit(state, "failed B", 41);
168 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s40", checkpointTurn: 5 } });
169 state = reducer(state, { type: "send_failed", submissionId: "s41", error: "turn already running" });
170 state = submit(state, "C after failed B", 42);
171 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s42", checkpointTurn: 6 } });
172
173 eq(checkpoint(state, "u40"), 5, "A maps before B rejection");
174 eq(userById(state, "u41")?.failed, true, "B rejection marks only B");
175 eq(checkpoint(state, "u41"), undefined, "failed B remains non-rewindable");
176 eq(checkpoint(state, "u42"), 6, "C maps normally after the rejected B gap");
177 }
178
179 {
180 let state = submit(initialState, "rapid A", 50);
181 state = reducer(state, { type: "unsend" });
182 state = submit(state, "rapid B", 51);
183 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s51", checkpointTurn: 8 } });
184 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s50", checkpointTurn: 7 } });
185
186 eq(checkpoint(state, "u50"), 7, "out-of-order A completion still maps A");
187 eq(checkpoint(state, "u51"), 8, "out-of-order B completion still maps B");
188 }
189
190 {
191 let state = submit(initialState, "!pwd", 60);
192 state = reducer(state, { type: "event", e: { kind: "turn_done" } });
193 eq(checkpoint(state, "u60"), undefined, "shell TurnDone without submission id stays non-rewindable");
194
195 state = submit(state, "after shell", 61);
196 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s61", checkpointTurn: 9 } });
197 eq(checkpoint(state, "u61"), 9, "shell completion cannot offset the following normal turn");
198 }
199
200 {
201 let state = submit(initialState, "provider error", 70);
202 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s70", checkpointTurn: 10, err: "provider unavailable" } });
203 eq(checkpoint(state, "u70"), 10, "error TurnDone maps an admitted checkpoint by id");
204 eq(state.items.some((item) => item.kind === "notice" && item.text === "provider unavailable"), true, "error TurnDone still surfaces its warning");
205 }
206
207 {
208 let state = submit(initialState, "same text", 80);
209 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s80", checkpointTurn: 11 } });
210 state = submit(state, "same text", 81);
211 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s81", checkpointTurn: 12 } });
212 eq(checkpoint(state, "u80"), 11, "first equal-text user keeps its checkpoint");
213 eq(checkpoint(state, "u81"), 12, "second equal-text user maps independently by id");
214 }
215
216 {
217 let state = submit(initialState, "old pending", 90);
218 state = reducer(state, { type: "unsend" });
219 state = submit(state, "new pending", 91);
220 const unchanged = state;
221 state = reducer(state, { type: "send_failed", submissionId: "s90", error: "late old rejection" });
222 eq(state.pendingSubmissionId, unchanged.pendingSubmissionId, "old rejection preserves the newer pending id");
223 eq(state.running, unchanged.running, "old rejection preserves the newer runtime");
224 eq(state.localSubmissions.s91, unchanged.localSubmissions.s91, "old rejection leaves the newer echo unchanged");
225 eq(userById(state, "u90")?.failed, true, "old rejection settles only its own retained echo");
226 state = reducer(state, { type: "send_failed", submissionId: "s91", error: "current rejection" });
227 eq(userById(state, "u91")?.failed, true, "current send_failed marks the exact pending user");
228 eq(userById(state, "u91")?.submissionId, "s91", "send_failed preserves durable identity for a late canonical receipt");
229 eq(userById(state, "u90")?.failed, true, "current rejection preserves the older echo status");
230 }
231
232 {
233 let state = submit(initialState, "confirmed delivery", 100);
234 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s100", checkpointTurn: 13 } });
235 const confirmed = state;
236 state = reducer(state, { type: "send_failed", submissionId: "s100", error: "late bridge rejection" });
237 eq(state, confirmed, "send_failed after matching TurnDone is a complete no-op");
238 eq(checkpoint(state, "u100"), 13, "late failure cannot erase an assigned checkpoint");
239 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s100", checkpointTurn: 99 } });
240 eq(checkpoint(state, "u100"), 13, "duplicate TurnDone cannot overwrite an existing checkpoint");
241 }
242
243 {
244 let state = submit(initialState, "reset prompt", 110);
245 state = reducer(state, { type: "reset" });
246 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s110", checkpointTurn: 14 } });
247 eq(state.items.length, 0, "session reset removes the old optimistic id target");
248
249 state = submit(state, "new runtime prompt", 111);
250 state = reducer(state, { type: "controller_rebuilt" });
251 eq(localById(state, "u111")?.settled, true, "controller rebuild retires the old checkpoint authority");
252 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s111", checkpointTurn: 99 } });
253 eq(checkpoint(state, "u111"), undefined, "late pre-rebuild TurnDone cannot stamp the retired item");
254 state = submit(state, "post-rebuild prompt", 112);
255 state = reducer(state, { type: "send_failed", submissionId: "s111", error: "old runtime rejection" });
256 eq(state.pendingSubmissionId, "s112", "old-runtime failure cannot clear the rebuilt runtime pending correlation");
257 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s112", checkpointTurn: 15 } });
258 eq(checkpoint(state, "u112"), 15, "post-rebuild submission maps normally");
259 }
260
261 {
262 let state = reducer(initialState, {
263 type: "history_page",
264 mode: "replace",
265 page: { messages: [{ role: "user", content: "recent", checkpointTurn: 60 }], startTurn: 60, endTurn: 61, totalTurns: 61, hasOlder: true },
266 });
267 state = submit(state, "live after pagination", 120);
268 state = reducer(state, {
269 type: "history_page",
270 mode: "prepend",
271 page: { messages: [{ role: "user", content: "older", checkpointTurn: 5 }], startTurn: 5, endTurn: 6, totalTurns: 61, hasOlder: true },
272 });
273 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s120", checkpointTurn: 61 } });
274 eq(checkpoint(state, "u120"), 61, "history prepend preserves the exact live item target");
275 eq(state.items.some((item) => item.kind === "user" && item.text === "recent" && item.checkpointTurn === 60), true, "recent history keeps authoritative metadata");
276 eq(state.items.some((item) => item.kind === "user" && item.text === "older" && item.checkpointTurn === 5), true, "prepended history keeps authoritative metadata");
277 }
278
279 {
280 let state = reducer(initialState, {
281 type: "history_page",
282 mode: "replace",
283 page: { messages: [{ role: "user", content: "authoritative", checkpointTurn: 7 }], startTurn: 7, endTurn: 8, totalTurns: 8, hasOlder: false },
284 });
285 state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "u999", checkpointTurn: 99 } });
286 eq(state.items[0].kind === "user" && state.items[0].checkpointTurn, 7, "unknown local id cannot rewrite hydrated history");
287 }
288
289 {
290 const source = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../lib/useController.ts"), "utf8");
291 eq(source.includes("turnUserItemIds"), false, "FIFO ownership state is completely removed");
292 eq(source.includes("HistoryCheckpointTurnsForTab(targetTabId)"), false, "TurnDone hot path does not refresh full checkpoint history");
293 eq(source.includes('type: "history_checkpoint_turns"'), false, "positional checkpoint merge action remains removed");
294 eq(source.includes("void refreshTurnBoundaries(targetTabId)"), true, "TurnDone still refreshes checkpoint metadata");
295 }
296
297 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
298 if (failed > 0) process.exit(1);
299
299 lines TYPESCRIPT