返回 DeepSeek-Reasonix
session-recovery-version-host-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / session-recovery-version-host-lifecycle.test.tsx
1 import { JSDOM } from "jsdom";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import type { ProjectTreeChangedV2 } from "../lib/sessionCatalogTypes";
5 import type { SessionRecoveryEvent } from "../lib/types";
6 import { installDesktopHostStub } from "./desktopHostStub";
7
8 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", { pretendToBeVisual: true, url: "http://localhost/" });
9 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10 globalThis.window = dom.window as unknown as Window & typeof globalThis;
11 globalThis.document = dom.window.document;
12 Object.defineProperty(dom.window.navigator, "language", { configurable: true, value: "en-US" });
13 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
14 globalThis.Node = dom.window.Node;
15 globalThis.Element = dom.window.Element;
16 globalThis.HTMLElement = dom.window.HTMLElement;
17 globalThis.Event = dom.window.Event;
18 globalThis.MouseEvent = dom.window.MouseEvent;
19 globalThis.localStorage = dom.window.localStorage;
20
21 const lineageReads: Array<Record<string, unknown>> = [];
22 const desktopStub = installDesktopHostStub(({
23 main: {
24 App: {
25 GetRecoveryLineage: async (key: Record<string, unknown>) => {
26 lineageReads.push(key);
27 return {
28 groupId: "root",
29 state: "diverged",
30 branchCount: 2,
31 unresolved: 1,
32 cleanupEligible: 0,
33 members: [
34 { path: "/sessions/root.jsonl", role: "normal", canonical: true, turns: 1, open: false, running: false },
35 { path: "/sessions/fork.jsonl", role: "diverged", canonical: false, turns: 2, open: false, running: false },
36 ],
37 };
38 },
39 },
40 },
41 }).main.App);
42
43 const { LocaleProvider, preloadLocale, useI18n } = await import("../lib/i18n");
44 const { ToastProvider } = await import("../lib/toast");
45 await preloadLocale("zh");
46 await import("../lib/sessionRecoveryRuntime");
47 const { SessionRecoveryVersionsHost } = await import("../components/SessionRecoveryVersionsHost");
48
49 let setLocale: ReturnType<typeof useI18n>["setPref"] | undefined;
50 function LocaleControl() {
51 setLocale = useI18n().setPref;
52 return null;
53 }
54
55 function emit(name: string, payload: SessionRecoveryEvent | ProjectTreeChangedV2) {
56 desktopStub.emit(name, payload);
57 }
58
59 async function flushMicrotasks() {
60 await Promise.resolve();
61 await Promise.resolve();
62 }
63
64 let recovered = 0;
65 const root = createRoot(document.getElementById("root")!);
66 await act(async () => {
67 root.render(
68 <LocaleProvider>
69 <ToastProvider>
70 <LocaleControl />
71 <SessionRecoveryVersionsHost
72 sessions={[]}
73 onResumeSession={async () => {}}
74 onRecoveryCreated={() => { recovered += 1; }}
75 onLineageChanged={() => {}}
76 />
77 </ToastProvider>
78 </LocaleProvider>,
79 );
80 await flushMicrotasks();
81 });
82
83 if ((desktopStub.events.get("session:recovered")?.size ?? 0) !== 1 || (desktopStub.events.get("project-tree:changed-v2")?.size ?? 0) !== 1) {
84 throw new Error("recovery runtime did not install exactly one subscription pair");
85 }
86
87 await act(async () => {
88 emit("session:recovered", {
89 recoveryPath: "/sessions/fork.jsonl",
90 scope: "global",
91 topicId: "topic",
92 recoveryParentId: "root",
93 recoveryReason: "snapshot_conflict",
94 });
95 await flushMicrotasks();
96 });
97 if (recovered !== 1 || lineageReads.length !== 0) throw new Error("recovery event was not held pending for catalog classification");
98
99 await act(async () => {
100 setLocale?.("zh");
101 await flushMicrotasks();
102 });
103 if ((desktopStub.events.get("session:recovered")?.size ?? 0) !== 1 || (desktopStub.events.get("project-tree:changed-v2")?.size ?? 0) !== 1) {
104 throw new Error("locale change restarted the recovery runtime and lost pending state");
105 }
106
107 await act(async () => {
108 emit("project-tree:changed-v2", { revision: 2, roots: [""], reason: "reconcile" });
109 await flushMicrotasks();
110 });
111 if (lineageReads.length !== 1 || lineageReads[0].recordClassification !== true) {
112 throw new Error(`catalog classification was not persisted by the backend read: ${JSON.stringify(lineageReads)}`);
113 }
114 if (document.querySelectorAll(".toast").length !== 1) throw new Error("confirmed divergence did not produce exactly one toast after locale change");
115
116 await act(async () => {
117 emit("project-tree:changed-v2", { revision: 3, roots: [""], reason: "duplicate" });
118 await flushMicrotasks();
119 });
120 if (lineageReads.length !== 1 || document.querySelectorAll(".toast").length !== 1) {
121 throw new Error("resolved recovery event was processed more than once");
122 }
123
124 await act(async () => {
125 (document.querySelector(".toast") as HTMLElement).click();
126 root.unmount();
127 });
128 dom.window.close();
129 console.log(" PASS session recovery host preserves pending classification across locale changes");
130
130 lines Plain Text