返回 DeepSeek-Reasonix
maintenance-lifecycle.test.ts
根目录 / desktop / frontend / src / __tests__ / maintenance-lifecycle.test.ts
1 import assert from "node:assert/strict";
2 import { canonicalMessage } from "../lib/canonicalTranscriptBackend";
3 import type { RuntimeState } from "../lib/runtimeStateStore";
4 import { applyResolvedField, entryToRecord } from "../lib/transcriptRecordProjection";
5 import type { HistoryContentRef } from "../lib/types";
6 import { initialState, reducer } from "../lib/useController";
7 import type { Item, State } from "../lib/useController";
8
9 type CompactionItem = Extract<Item, { kind: "compaction" }>;
10
11 function operationCard(state: State, operationId: string): CompactionItem | undefined {
12 return state.items.find((item): item is CompactionItem => item.kind === "compaction" && item.operationId === operationId);
13 }
14
15 const runtime = (revision: number, maintenance: RuntimeState["maintenance"]): RuntimeState => ({
16 schemaVersion: 1,
17 projectionEpoch: "projection-1",
18 runtimeEpoch: "runtime-1",
19 activityRevision: revision,
20 revision,
21 phase: maintenance?.activity === "recovery_required" ? "recovery_required" : "executing",
22 running: Boolean(maintenance),
23 turnId: "",
24 turnStatus: "",
25 turnEventSeq: 0,
26 pendingPrompt: false,
27 cancelRequested: maintenance?.activity === "cancelling",
28 cancellable: Boolean(maintenance),
29 backgroundJobs: 0,
30 activity: maintenance ? "maintenance" : "idle",
31 maintenance,
32 });
33
34 let state = reducer(initialState, { type: "event", e: {
35 kind: "session_operation",
36 sessionOperation: { operationId: "op-1", kind: "compact", activity: "running", status: "running", inputTokens: 1200, operationRevision: 1, runtimeEpoch: "runtime-1" },
37 } });
38 let cards = state.items.filter(item => item.kind === "compaction");
39 assert.equal(cards.length, 1);
40 assert.equal(cards[0].pending, true);
41 assert.equal(cards[0].id, "maintenance:op-1");
42
43 state = reducer(state, { type: "event", e: {
44 kind: "session_operation",
45 sessionOperation: { operationId: "op-1", kind: "compact", activity: "cancelling", status: "cancelling", inputTokens: 1200, operationRevision: 2, runtimeEpoch: "runtime-1" },
46 } });
47 cards = state.items.filter(item => item.kind === "compaction");
48 assert.equal(cards.length, 1, "cancellation updates the stable operation card");
49 assert.equal(cards[0].status, "cancelling");
50
51 state = reducer(state, { type: "event", e: {
52 kind: "compaction_done",
53 compaction: { trigger: "manual", messages: 8, summary: "summary", archive: "archive.jsonl" },
54 } });
55 cards = state.items.filter(item => item.kind === "compaction");
56 assert.equal(cards.length, 1, "agent completion metadata does not create a second manual card");
57 assert.equal(cards[0].summary, "summary");
58
59 state = reducer(state, { type: "event", e: {
60 kind: "session_operation",
61 sessionOperation: { operationId: "op-1", kind: "compact", activity: "finalizing", status: "completed", applied: true, inputTokens: 1200, resultTokens: 400, messages: 8, summary: "summary", operationRevision: 4, runtimeEpoch: "runtime-1" },
62 } });
63 cards = state.items.filter(item => item.kind === "compaction");
64 assert.equal(cards.length, 1);
65 assert.equal(cards[0].pending, false);
66 assert.equal(cards[0].status, "completed");
67 assert.equal(cards[0].resultTokens, 400);
68
69 // An event delayed behind terminal persistence cannot regress the operation.
70 state = reducer(state, { type: "event", e: {
71 kind: "session_operation",
72 sessionOperation: { operationId: "op-1", kind: "compact", activity: "cancelling", status: "cancelling", operationRevision: 3, runtimeEpoch: "runtime-1" },
73 } });
74 cards = state.items.filter(item => item.kind === "compaction");
75 assert.equal(cards[0].status, "completed");
76 assert.equal(cards[0].pending, false);
77 assert.equal(cards[0].operationRevision, 4);
78
79 // A runtime refresh is a partial observation: omitted result fields retain the
80 // durable event values and a terminal card does not become pending again.
81 state = reducer(state, { type: "runtime_snapshot", snapshot: runtime(5, {
82 operationId: "op-1",
83 kind: "compact",
84 activity: "running",
85 operationRevision: 3,
86 runtimeEpoch: "runtime-1",
87 }) });
88 cards = state.items.filter(item => item.kind === "compaction");
89 assert.equal(cards[0].status, "completed");
90 assert.equal(cards[0].applied, true);
91 assert.equal(cards[0].inputTokens, 1200);
92 assert.equal(cards[0].resultTokens, 400);
93
94 let failed = reducer(initialState, { type: "event", e: {
95 kind: "session_operation",
96 sessionOperation: {
97 operationId: "op-failed", kind: "compact", activity: "recovery_required", status: "recovery_required",
98 errorCode: "save_failed", detail: "disk full", applied: true, inputTokens: 900, resultTokens: 350,
99 operationRevision: 7, runtimeEpoch: "runtime-1",
100 },
101 } });
102 failed = reducer(failed, { type: "runtime_snapshot", snapshot: runtime(8, {
103 operationId: "op-failed",
104 kind: "compact",
105 activity: "recovery_required",
106 operationRevision: 7,
107 runtimeEpoch: "runtime-1",
108 }) });
109 const failedCard = failed.items.filter(item => item.kind === "compaction").find(item => item.operationId === "op-failed");
110 assert.equal(failedCard?.detail, "disk full");
111 assert.equal(failedCard?.errorCode, "save_failed");
112 assert.equal(failedCard?.applied, true);
113 assert.equal(failedCard?.inputTokens, 900);
114 assert.equal(failedCard?.resultTokens, 350);
115
116 // session-maintenance-v1 stores a provider compaction row whose content is the
117 // operation JSON. Canonical history must recover the display fields, not show
118 // the JSON as a successful generic compaction.
119 const persisted = canonicalMessage({
120 messageId: "maintenance:op-history", position: 4, version: 1, role: "compaction", eventSequence: 9, visibleTurn: 2,
121 }, {
122 id: "maintenance:op-history",
123 role: "compaction",
124 content: JSON.stringify({
125 operationId: "op-history", kind: "compact", activity: "recovery_required", status: "recovery_required",
126 errorCode: "save_failed", detail: "could not save", applied: true, inputTokens: 1000, resultTokens: 450,
127 operationRevision: 6, runtimeEpoch: "runtime-history",
128 }),
129 });
130 assert.equal(persisted.role, "compaction");
131 assert.equal(persisted.operationId, "op-history");
132 assert.equal(persisted.operationStatus, "recovery_required");
133 assert.equal(persisted.errorCode, "save_failed");
134 assert.equal(persisted.detail, "could not save");
135 assert.equal(persisted.applied, true);
136 assert.equal(persisted.operationRevision, 6);
137 assert.equal(persisted.runtimeEpoch, "runtime-history");
138
139 // Large summaries are reference-backed. Their placeholder must be loading (or
140 // explicitly unavailable), never a false success, and hydration uses the same
141 // canonical parser as the inline row.
142 const lazySource = {
143 id: "maintenance:op-lazy",
144 role: "compaction",
145 content: JSON.stringify({
146 operationId: "op-lazy", kind: "compact", activity: "finalizing", status: "failed",
147 errorCode: "summary_failed", detail: "late body", summary: "x".repeat(5000),
148 operationRevision: 9, runtimeEpoch: "runtime-lazy",
149 }),
150 };
151 const lazyPersistent = {
152 messageId: "maintenance:op-lazy", position: 6, version: 1, role: "compaction", eventSequence: 11, visibleTurn: 2,
153 contentRef: { digest: "lazy", bytes: JSON.stringify(lazySource).length },
154 };
155 const lazyMessage = canonicalMessage(lazyPersistent, undefined);
156 assert.equal(lazyMessage.operationStatus, "loading");
157 assert.equal(lazyMessage.pending, true);
158 const lazyRef: HistoryContentRef = { entryId: "m:maintenance:op-lazy", field: "canonicalMessage", size: lazyPersistent.contentRef.bytes, chunks: 1, revision: 1, digest: "lazy" };
159 const lazyRecord = entryToRecord({ entryId: lazyRef.entryId, turn: 2, order: 6, message: lazyMessage, refs: [lazyRef] });
160 const lazyBytes = [...new TextEncoder().encode(JSON.stringify(lazySource))].map(byte => String.fromCharCode(byte)).join("");
161 assert.equal(applyResolvedField(lazyRecord, lazyRef, lazyBytes), true);
162 assert.equal(lazyRecord.message.operationStatus, "failed");
163 assert.equal(lazyRecord.message.errorCode, "summary_failed");
164 assert.equal(lazyRecord.message.detail, "late body");
165 assert.equal(lazyRecord.message.operationRevision, 9);
166
167 // History and live state use the operation id as one stable node identity.
168 let reconciled = reducer(initialState, { type: "history", messages: [persisted] });
169 reconciled = reducer(reconciled, { type: "runtime_snapshot", snapshot: runtime(1, {
170 operationId: "op-history", kind: "compact", activity: "recovery_required",
171 operationRevision: 6, runtimeEpoch: "runtime-history", status: "recovery_required",
172 }) });
173 const reconciledCards = reconciled.items.filter(item => item.kind === "compaction").filter(item => item.operationId === "op-history");
174 assert.equal(reconciledCards.length, 1);
175 assert.equal(reconciledCards[0].id, "maintenance:op-history");
176 assert.equal(reconciledCards[0].errorCode, "save_failed");
177
178 // The inverse arrival order is equally monotonic: an in-flight history read
179 // cannot replace a newer live terminal record with its older running row.
180 const staleHistory = canonicalMessage({
181 messageId: "maintenance:op-race", position: 5, version: 1, role: "compaction", eventSequence: 10, visibleTurn: 2,
182 }, {
183 id: "maintenance:op-race",
184 role: "compaction",
185 content: JSON.stringify({
186 operationId: "op-race", kind: "compact", activity: "running", status: "running",
187 inputTokens: 800, operationRevision: 1, runtimeEpoch: "runtime-1",
188 }),
189 });
190 let liveFirst = reducer(initialState, { type: "event", e: {
191 kind: "session_operation",
192 sessionOperation: {
193 operationId: "op-race", kind: "compact", activity: "finalizing", status: "failed",
194 errorCode: "summary_failed", detail: "provider failed", inputTokens: 800, resultTokens: 800,
195 operationRevision: 3, runtimeEpoch: "runtime-1",
196 },
197 } });
198 liveFirst = reducer(liveFirst, { type: "history", messages: [staleHistory] });
199 const liveFirstCards = liveFirst.items.filter(item => item.kind === "compaction").filter(item => item.operationId === "op-race");
200 assert.equal(liveFirstCards.length, 1);
201 assert.equal(liveFirstCards[0].status, "failed");
202 assert.equal(liveFirstCards[0].detail, "provider failed");
203 assert.equal(liveFirstCards[0].operationRevision, 3);
204
205 // A persisted progress row stays pending until runtime synchronization can
206 // prove whether its operation still exists. Both arrival orders converge on
207 // interrupted when the synchronized runtime is idle.
208 let historyBeforeRuntime = reducer(initialState, { type: "history", messages: [staleHistory] });
209 let historyBeforeRuntimeCard = operationCard(historyBeforeRuntime, "op-race");
210 assert.equal(historyBeforeRuntimeCard?.pending, true);
211 assert.equal(historyBeforeRuntimeCard?.status, "confirming");
212 historyBeforeRuntime = reducer(historyBeforeRuntime, { type: "runtime_snapshot", snapshot: runtime(11, undefined) });
213 historyBeforeRuntimeCard = operationCard(historyBeforeRuntime, "op-race");
214 assert.equal(historyBeforeRuntimeCard?.pending, false);
215 assert.equal(historyBeforeRuntimeCard?.status, "interrupted");
216
217 let runtimeBeforeHistory = reducer(initialState, { type: "runtime_snapshot", snapshot: runtime(12, undefined) });
218 runtimeBeforeHistory = reducer(runtimeBeforeHistory, { type: "history", messages: [staleHistory] });
219 const runtimeBeforeHistoryCards = runtimeBeforeHistory.items.filter((item): item is CompactionItem => item.kind === "compaction" && item.operationId === "op-race");
220 assert.equal(runtimeBeforeHistoryCards.length, 1);
221 assert.equal(runtimeBeforeHistoryCards[0].pending, false);
222 assert.equal(runtimeBeforeHistoryCards[0].status, "interrupted");
223
224 // A matching active identity retains its running state after synchronization.
225 let activeRecovery = reducer(initialState, { type: "history", messages: [staleHistory] });
226 activeRecovery = reducer(activeRecovery, { type: "runtime_snapshot", snapshot: runtime(13, {
227 operationId: "op-race", kind: "compact", activity: "running", status: "running",
228 operationRevision: 1, runtimeEpoch: "runtime-1",
229 }) });
230 const activeRecoveryCards = activeRecovery.items.filter((item): item is CompactionItem => item.kind === "compaction" && item.operationId === "op-race");
231 assert.equal(activeRecoveryCards.length, 1);
232 assert.equal(activeRecoveryCards[0].pending, true);
233 assert.equal(activeRecoveryCards[0].status, "running");
234
235 console.log("maintenance lifecycle: persisted parsing, monotonic merge and reconciliation passed");
236
237 // A previously cached idle snapshot is not evidence that a newer live
238 // operation has stopped. History refresh must not manufacture a terminal.
239 let cachedIdle = reducer(initialState, { type: "runtime_snapshot", snapshot: runtime(20, undefined) });
240 const freshOp = { operationId: "op-race", kind: "compact", activity: "running", status: "running", operationRevision: 1, runtimeEpoch: "runtime-1" };
241 cachedIdle = reducer(cachedIdle, { type: "event", e: { kind: "session_operation", sessionOperation: freshOp } });
242 cachedIdle = reducer(cachedIdle, { type: "history", messages: [staleHistory] });
243 assert.equal(operationCard(cachedIdle, "op-race")?.status, "running");
244 cachedIdle = reducer(cachedIdle, { type: "runtime_snapshot", snapshot: runtime(21, freshOp) });
245 assert.equal(operationCard(cachedIdle, "op-race")?.status, "running");
246
247 // An inferred interruption is reversible when fresh authoritative evidence
248 // arrives; a persisted interruption remains a real terminal state.
249 historyBeforeRuntime = reducer(historyBeforeRuntime, { type: "runtime_snapshot", snapshot: runtime(22, freshOp) });
250 assert.equal(operationCard(historyBeforeRuntime, "op-race")?.status, "running");
251
252 for (const status of ["future_state", ""]) {
253 const unknown = reducer(initialState, { type: "event", e: { kind: "session_operation", sessionOperation: { ...freshOp, status, activity: "" } } });
254 assert.equal(operationCard(unknown, "op-race")?.status, "unavailable");
255 assert.equal(operationCard(unknown, "op-race")?.pending, false);
256 }
257
258 let formalInterruption = reducer(initialState, { type: "event", e: { kind: "session_operation", sessionOperation: { ...freshOp, status: "interrupted", operationRevision: 2 } } });
259 formalInterruption = reducer(formalInterruption, { type: "runtime_snapshot", snapshot: runtime(23, { ...freshOp, operationRevision: 3 }) });
260 assert.equal(operationCard(formalInterruption, "op-race")?.status, "interrupted", "formal terminal states remain irreversible");
261 const unknownSaving = reducer(initialState, { type: "event", e: { kind: "session_operation", sessionOperation: { ...freshOp, status: "future_state", activity: "finalizing" } } });
262 assert.equal(operationCard(unknownSaving, "op-race")?.status, "unavailable", "unknown status cannot inherit a familiar activity");
263
263 lines TYPESCRIPT