返回 DeepSeek-Reasonix
recovery-lineage-dialog.test.tsx
根目录 / desktop / frontend / src / __tests__ / recovery-lineage-dialog.test.tsx
1 import { JSDOM } from "jsdom";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import type { AppBindings } from "../lib/bridge";
5 import type { RecoveryLineageView } 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.HTMLButtonElement = dom.window.HTMLButtonElement;
18 globalThis.Event = dom.window.Event;
19 globalThis.MouseEvent = dom.window.MouseEvent;
20 globalThis.localStorage = dom.window.localStorage;
21
22 const { RecoveryLineageDialog } = await import("../components/RecoveryLineageDialog");
23 const { LocaleProvider } = await import("../lib/i18n");
24 const { ToastProvider } = await import("../lib/toast");
25 const { setFrontendDiagnosticSink } = await import("../lib/frontendDiagnosticBridge");
26
27 const initial: RecoveryLineageView = {
28 groupId: "group",
29 state: "diverged",
30 branchCount: 3,
31 unresolved: 1,
32 cleanupEligible: 1,
33 members: [
34 { path: "/private/root.jsonl", role: "normal", canonical: true, turns: 3, open: true, running: false, preview: "original preview", lastActivityAt: 100 },
35 { path: "/private/covered.jsonl", role: "covered_copy", canonical: false, turns: 3, open: false, running: false, preview: "covered preview" },
36 { path: "/private/fork.jsonl", role: "diverged", canonical: false, turns: 4, open: false, running: false, preview: "unique preview", versionNote: "investigation" },
37 ],
38 };
39
40 let opened = "";
41 const root = createRoot(document.getElementById("root")!);
42 await act(async () => {
43 root.render(
44 <LocaleProvider>
45 <RecoveryLineageDialog
46 topic={{ scope: "global", topicId: "topic" }}
47 initial={initial}
48 onClose={() => {}}
49 onChanged={() => {}}
50 onOpenVersion={(member) => { opened = member.path; }}
51 />
52 </LocaleProvider>,
53 );
54 });
55
56 const body = document.body.textContent || "";
57 if (!body.includes("Session versions") || !body.includes("Default version") || !body.includes("Another version")) throw new Error("dialog does not use the simplified version language");
58 if (!body.includes("original preview") || !body.includes("unique preview") || !body.includes("investigation")) throw new Error("dialog omits user-facing version metadata");
59 if (body.includes("covered preview") || body.includes("covered_copy")) throw new Error("covered copies leaked into the ordinary version dialog");
60 if (body.includes("/private/") || body.includes(".jsonl")) throw new Error("physical session paths leaked into the dialog");
61 if (body.includes("cleanup") || body.includes("Move covered")) throw new Error("ordinary version dialog exposes cleanup controls");
62
63 const openButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("button")).filter((button) => button.textContent?.trim() === "Open this version");
64 if (openButtons.length !== 2) throw new Error(`expected 2 visible version actions, got ${openButtons.length}`);
65 await act(async () => { openButtons[1].click(); });
66 if (opened !== "/private/fork.jsonl") throw new Error("open action was not bound to the selected version");
67
68 await act(async () => root.unmount());
69 console.log(" PASS session version dialog hides persistence details");
70
71 // A rejected backend call must end as a toast, never as an unhandledrejection
72 // (which the global handler would otherwise paint as a full-screen crash).
73 const rejections: unknown[] = [];
74 const onProcessRejection = (reason: unknown) => { rejections.push(reason); };
75 process.on("unhandledRejection", onProcessRejection);
76 const diagnostics: string[] = [];
77 setFrontendDiagnosticSink((_source, type) => { diagnostics.push(type); });
78 installDesktopHostStub(({
79 main: {
80 App: {
81 ChooseRecoveryBranch: () => Promise.reject("selected branch is outside the recovery lineage"),
82 GetRecoveryLineage: async () => initial,
83 } as Partial<AppBindings> as AppBindings,
84 },
85 }).main.App);
86
87 let closed = false;
88 const failing = createRoot(document.getElementById("root")!);
89 await act(async () => {
90 failing.render(
91 <LocaleProvider>
92 <ToastProvider>
93 <RecoveryLineageDialog
94 topic={{ scope: "global", topicId: "topic" }}
95 initial={initial}
96 onClose={() => { closed = true; }}
97 onChanged={() => {}}
98 onOpenVersion={() => Promise.reject(new Error("resume failed"))}
99 />
100 </ToastProvider>
101 </LocaleProvider>,
102 );
103 });
104 const findButton = (label: string) => Array.from(document.querySelectorAll<HTMLButtonElement>("button")).find((button) => button.textContent?.trim() === label);
105 const errorToasts = () => Array.from(document.querySelectorAll(".toast--error .toast__text")).map((node) => node.textContent);
106
107 await act(async () => { findButton("Set as default version")!.click(); });
108 await act(async () => { await Promise.resolve(); });
109 if (!errorToasts().includes("Could not set the default version: selected branch is outside the recovery lineage")) {
110 throw new Error(`rejected default-version choice did not toast: ${JSON.stringify(errorToasts())}`);
111 }
112 if (!diagnostics.includes("session.recovery-choose-failed")) throw new Error("rejected default-version choice did not record a diagnostic");
113 if (findButton("Set as default version")?.disabled) throw new Error("dialog stayed busy after the rejected choice");
114
115 await act(async () => { findButton("Open this version")!.click(); });
116 await act(async () => { await Promise.resolve(); });
117 if (!errorToasts().includes("Could not open this version: resume failed")) {
118 throw new Error(`rejected open-version did not toast: ${JSON.stringify(errorToasts())}`);
119 }
120 if (closed) throw new Error("dialog closed although opening the version failed");
121
122 await act(async () => failing.unmount());
123 await new Promise((resolve) => setTimeout(resolve, 0));
124 process.off("unhandledRejection", onProcessRejection);
125 if (rejections.length > 0) throw new Error(`recovery dialog leaked unhandled rejections: ${JSON.stringify(rejections)}`);
126 console.log(" PASS session version dialog contains rejected backend calls as toasts");
127
128 dom.window.close();
129
129 lines Plain Text