返回 DeepSeek-Reasonix
hydrate-history-apply.test.ts
根目录 / desktop / frontend / src / __tests__ / hydrate-history-apply.test.ts
1 // Run: tsx src/__tests__/hydrate-history-apply.test.ts
2
3 import {
4 activeTabHydrationPlan,
5 canAdoptUnboundLiveSurface,
6 duplicateLiveItemIds,
7 hasCachedLiveTurn,
8 hydratedHistoryApplyMode,
9 sameSessionHydrateIdentity,
10 sameSessionPlaceholderItems,
11 shouldPreferResidentHistory,
12 } from "../lib/hydrateHistoryApply";
13
14 let passed = 0;
15 let failed = 0;
16
17 function ok(value: boolean, label: string) {
18 if (value) {
19 process.stdout.write(` PASS ${label}\n`);
20 passed += 1;
21 } else {
22 process.stdout.write(` FAIL ${label}\n`);
23 failed += 1;
24 }
25 }
26
27 console.log("\nhydrate history apply");
28
29 const mode = hydratedHistoryApplyMode;
30
31 ok(mode(true, true, false, { items: [] }) === "skip", "skipHistory blocks apply");
32 ok(mode(false, false, false, { items: [] }) === "skip", "missing projection blocks apply");
33 ok(mode(false, true, false, { items: [] }) === "replace", "idle empty surface applies history");
34 ok(mode(false, true, true, { running: true, items: [] }) === "replace", "running empty surface applies history");
35 ok(
36 mode(false, true, true, { running: true, live: { text: "partial" }, items: [] }) === "replace",
37 "a mid-stream surface with no rows yet applies history",
38 );
39 ok(
40 mode(false, true, true, { running: true, items: [{ kind: "user" }] }) === "prepend",
41 "a running turn with no history page behind it gets one prepended",
42 );
43 ok(
44 mode(false, true, true, { running: true, historyTotalTurns: 3, items: [{ kind: "user" }] }) === "skip",
45 "an already-hydrated running transcript is left alone",
46 );
47 ok(
48 hasCachedLiveTurn({
49 running: true,
50 historyTotalTurns: 2,
51 items: [{ kind: "assistant", streaming: true }],
52 }),
53 "streaming assistant counts as a cached live turn",
54 );
55 ok(
56 !hasCachedLiveTurn({ running: true, items: [{ kind: "assistant", streaming: true }] }),
57 "a live turn with no history page behind it is not cached",
58 );
59 ok(
60 duplicateLiveItemIds(
61 [{ kind: "user", id: "h1", text: "ask" }],
62 [{ kind: "user", id: "l1", text: "ask" }, { kind: "assistant", id: "l2", text: "" }],
63 ).join(",") === "l1",
64 "a live row the page already carries is dropped",
65 );
66 ok(
67 duplicateLiveItemIds(
68 [{ kind: "user", id: "h1", text: "ask" }],
69 [{ kind: "assistant", id: "l2", text: "" }],
70 ).length === 0,
71 "a live tail the page does not carry is kept",
72 );
73 ok(
74 sameSessionPlaceholderItems({ sessionPath: "a.jsonl" }, { meta: { sessionPath: "b.jsonl" }, items: [{ kind: "user" }] }) === undefined,
75 "foreign session items are not placeholders",
76 );
77 ok(
78 (sameSessionPlaceholderItems({ sessionPath: "a.jsonl" }, { meta: { sessionPath: "a.jsonl" }, items: [{ kind: "user" }] }) ?? []).length === 1,
79 "same-session items stay placeholders",
80 );
81 ok(
82 sameSessionHydrateIdentity(
83 { sessionPath: "a.jsonl", sessionGeneration: 3 },
84 { sessionPath: "a.jsonl", sessionGeneration: 3 },
85 ),
86 "same path and generation prove the same session",
87 );
88 ok(
89 !sameSessionHydrateIdentity(
90 { sessionPath: "a.jsonl", sessionGeneration: 4 },
91 { sessionPath: "a.jsonl", sessionGeneration: 3 },
92 ),
93 "different generations reject placeholders even when paths match",
94 );
95 ok(
96 !sameSessionHydrateIdentity({ sessionPath: "" }, { sessionPath: "" }),
97 "empty identities cannot prove the same session",
98 );
99 ok(
100 sameSessionHydrateIdentity(
101 { session: { hostId: "local", sessionId: "canonical-a" }, sessionPath: "", sessionGeneration: 3 },
102 { session: { hostId: "local", sessionId: "canonical-a" }, sessionPath: "", sessionGeneration: 3 },
103 ),
104 "matching SessionRefs prove a canonical session even when both paths are empty",
105 );
106 ok(
107 !sameSessionHydrateIdentity(
108 { session: { hostId: "local", sessionId: "canonical-a" }, sessionPath: "" },
109 { session: { hostId: "local", sessionId: "canonical-b" }, sessionPath: "" },
110 ),
111 "different canonical SessionRefs never share an empty-path surface",
112 );
113 ok(
114 !sameSessionHydrateIdentity(
115 { session: { hostId: "local", sessionId: "canonical-a" }, sessionPath: "same.jsonl" },
116 { session: { hostId: "local", sessionId: "canonical-b" }, sessionPath: "same.jsonl" },
117 ),
118 "SessionRef disagreement outranks a matching compatibility path",
119 );
120 const sameSessionPlan = activeTabHydrationPlan(
121 { sessionPath: "a.jsonl", sessionGeneration: 3, sessionRevision: 8, sessionDigest: "rev-8" },
122 { sessionPath: "a.jsonl", sessionGeneration: 3 },
123 false,
124 );
125 ok(sameSessionPlan.surfacePolicy === "preserve-current", "backend sync preserves a proven same-session surface");
126 ok(sameSessionPlan.loadOptions.preserveCachedHistory, "same-session backend sync may reuse its resident history");
127 const reboundPlan = activeTabHydrationPlan(
128 { sessionPath: "a.jsonl", sessionGeneration: 4, sessionRevision: 9, sessionDigest: "rev-9" },
129 { sessionPath: "a.jsonl", sessionGeneration: 3 },
130 false,
131 );
132 ok(reboundPlan.surfacePolicy === "replace-surface", "backend sync replaces a generation-rebound surface");
133 ok(reboundPlan.loadOptions.sessionGeneration === 4, "replace-surface hydration carries the target generation fence");
134 ok(
135 canAdoptUnboundLiveSurface(
136 { sessionPath: "a.jsonl", sessionGeneration: 3 },
137 undefined,
138 { running: true, live: { text: "partial" }, items: [{ kind: "assistant", streaming: true }] },
139 true,
140 ),
141 "an unbound live runtime tail can be adopted before its first metadata snapshot",
142 );
143 ok(
144 !canAdoptUnboundLiveSurface(
145 { sessionPath: "a.jsonl" },
146 { sessionPath: "b.jsonl" },
147 { running: true, live: { text: "stale" }, items: [{ kind: "assistant", streaming: true }] },
148 true,
149 ),
150 "a differently identified surface cannot be adopted as a live tail",
151 );
152 ok(
153 !canAdoptUnboundLiveSurface(
154 { sessionPath: "a.jsonl" },
155 undefined,
156 { running: true, historyTotalTurns: 1, hydrateHistoryLoaded: true, items: [{ kind: "assistant", streaming: true }] },
157 true,
158 ),
159 "a surface with persisted history is never adopted without session identity",
160 );
161 ok(
162 !canAdoptUnboundLiveSurface(
163 { sessionPath: "a.jsonl" },
164 undefined,
165 { running: true, live: { text: "stale epoch" }, items: [{ kind: "assistant", streaming: true }] },
166 true,
167 "runtime-new",
168 "runtime-old",
169 ),
170 "a mismatched runtime epoch rejects an unbound live tail",
171 );
172
173 ok(
174 !shouldPreferResidentHistory(false, false),
175 "retry / explicit no-cache hydrates must not serve the resident snapshot",
176 );
177 ok(shouldPreferResidentHistory(false, true), "preserveCachedHistory still allows a resident hit");
178 ok(shouldPreferResidentHistory(false, undefined), "unspecified preserveCachedHistory still allows a resident hit");
179 ok(!shouldPreferResidentHistory(true, true), "reset hydrates never prefer the resident snapshot");
180
181 const liveIdle = {
182 items: [{ kind: "user" }, { kind: "assistant" }, { kind: "user" }],
183 historyRevision: 10,
184 historyDigest: "rev-10",
185 };
186 ok(
187 mode(false, true, false, liveIdle, {
188 items: [{ kind: "user" }],
189 revision: 10,
190 digest: "rev-10",
191 }) === "skip",
192 "idle transcript is not replaced by a shorter same-fingerprint resident snapshot",
193 );
194 ok(
195 mode(false, true, false, liveIdle, {
196 items: [{ kind: "user" }, { kind: "assistant" }, { kind: "user" }, { kind: "assistant" }],
197 revision: 11,
198 digest: "rev-11",
199 }) === "replace",
200 "a newer backend page still replaces the idle transcript",
201 );
202 ok(
203 mode(false, true, false, { items: [] }, {
204 items: [{ kind: "user" }],
205 revision: 10,
206 digest: "rev-10",
207 }) === "replace",
208 "empty idle surface still applies history",
209 );
210
211 console.log(`\n${passed} passed, ${failed} failed`);
212 if (failed > 0) process.exit(1);
213
213 lines TYPESCRIPT