返回 DeepSeek-Reasonix
crash-rejection-containment.test.ts
根目录 / desktop / frontend / src / __tests__ / crash-rejection-containment.test.ts
1 // Run: tsx src/__tests__/crash-rejection-containment.test.ts
2 //
3 // A rejected desktop bridge call arrives as a bare string (or a stackless object). That
4 // is an ordinary backend error and must never paint the full-screen crash
5 // overlay; only rejections carrying a real stack keep the crash surface.
6
7 import { JSDOM } from "jsdom";
8
9 const dom = new JSDOM("<!doctype html><html><body></body></html>", { url: "http://localhost/" });
10 globalThis.window = dom.window as unknown as Window & typeof globalThis;
11 globalThis.document = dom.window.document;
12 globalThis.CustomEvent = dom.window.CustomEvent;
13 globalThis.Event = dom.window.Event;
14 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
15
16 const { installGlobalCrashHandlers, isRecoverableRejectionReason, onRecoverableError } = await import("../lib/globalCrashHandlers");
17 const { setFrontendDiagnosticSink } = await import("../lib/frontendDiagnosticBridge");
18
19 let passed = 0;
20 let failed = 0;
21
22 function ok(cond: boolean, label: string) {
23 if (cond) {
24 process.stdout.write(` PASS ${label}\n`);
25 passed += 1;
26 } else {
27 process.stdout.write(` FAIL ${label}\n`);
28 failed += 1;
29 }
30 }
31
32 const diagnostics: string[] = [];
33 setFrontendDiagnosticSink((_source, type) => { diagnostics.push(type); });
34 const toasts: string[] = [];
35 onRecoverableError(({ message }) => { toasts.push(message); });
36 const originalConsoleError = console.error;
37 console.error = () => {};
38
39 // Mirrors main.tsx: bridge-level filters are installed before the crash handlers.
40 window.addEventListener("unhandledrejection", (e) => { if (e.reason === "bridge-filtered") e.preventDefault(); });
41 installGlobalCrashHandlers();
42
43 function rejectWith(reason: unknown): Event {
44 const event = new dom.window.Event("unhandledrejection", { cancelable: true });
45 Object.defineProperty(event, "reason", { value: reason });
46 window.dispatchEvent(event);
47 return event;
48 }
49 const overlay = () => document.getElementById("crash-overlay");
50
51 console.log("\ncrash rejection containment");
52
53 const stacked = new TypeError("renderer fault");
54 stacked.stack = "TypeError: renderer fault\n at render (src/App.tsx:12:3)";
55 ok(isRecoverableRejectionReason("selected branch is outside the recovery lineage"), "a bare Go error string is recoverable");
56 ok(isRecoverableRejectionReason({ message: "stackless error-like object" }), "a stackless error-like object is recoverable");
57 ok(isRecoverableRejectionReason(undefined), "an undefined reason is recoverable");
58 ok(!isRecoverableRejectionReason(stacked), "an Error with a stack is a genuine crash");
59
60 const backend = rejectWith("selected branch is outside the recovery lineage");
61 ok(backend.defaultPrevented, "a backend rejection is marked handled");
62 ok(overlay() === null, "a backend rejection does not paint the crash overlay");
63 ok(
64 toasts.length === 1 && toasts[0] === "selected branch is outside the recovery lineage",
65 "a backend rejection dispatches the recoverable-error event with its message",
66 );
67 ok(diagnostics.includes("unhandled-backend-rejection"), "a backend rejection records a frontend diagnostic");
68
69 rejectWith('remote tab "remote-1" status was superseded by newer runtime state');
70 ok(overlay() === null && toasts.length === 1, "existing crash suppressions still short-circuit before containment");
71
72 rejectWith("bridge-filtered");
73 ok(overlay() === null && toasts.length === 1, "a rejection already handled by a bridge filter is left alone");
74
75 const crash = rejectWith(stacked);
76 ok(!crash.defaultPrevented, "a stacked Error rejection stays unhandled for the crash surface");
77 ok((overlay()?.textContent ?? "").includes("renderer fault"), "a stacked Error rejection still paints the crash overlay");
78 ok(toasts.length === 1, "a stacked Error rejection is not downgraded to a toast");
79
80 overlay()?.remove();
81 window.dispatchEvent(new dom.window.ErrorEvent("error", { message: "renderer fault", error: stacked, cancelable: true }));
82 ok((overlay()?.textContent ?? "").includes("renderer fault"), "window.error still paints the crash overlay");
83
84 console.error = originalConsoleError;
85 dom.window.close();
86 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
87 if (failed > 0) process.exit(1);
88
88 lines TYPESCRIPT