返回 DeepSeek-Reasonix
goal-action-errors.test.tsx
根目录 / desktop / frontend / src / __tests__ / goal-action-errors.test.tsx
1 // Run: tsx src/__tests__/goal-action-errors.test.tsx
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6 import { JSDOM } from "jsdom";
7 import React, { act } from "react";
8 import { createRoot } from "react-dom/client";
9 import { useGoalActionHandler } from "../lib/goalAction";
10 import { ToastProvider } from "../lib/toast";
11
12 let passed = 0;
13 let failed = 0;
14
15 function ok(value: boolean, label: string) {
16 if (value) {
17 process.stdout.write(` PASS ${label}\n`);
18 passed += 1;
19 } else {
20 process.stdout.write(` FAIL ${label}\n`);
21 failed += 1;
22 }
23 }
24
25 function flushPromises(): Promise<void> {
26 return new Promise((resolve) => setTimeout(resolve, 0));
27 }
28
29 console.log("\ngoal action bridge errors");
30
31 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
32 pretendToBeVisual: true,
33 url: "http://localhost/",
34 });
35 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
36 globalThis.window = dom.window as unknown as Window & typeof globalThis;
37 globalThis.document = dom.window.document;
38 globalThis.Event = dom.window.Event;
39 globalThis.MouseEvent = dom.window.MouseEvent;
40
41 const unhandled: unknown[] = [];
42 const onUnhandledRejection = (reason: unknown) => unhandled.push(reason);
43 const onWindowUnhandledRejection = (event: PromiseRejectionEvent) => unhandled.push(event.reason);
44 process.on("unhandledRejection", onUnhandledRejection);
45 window.addEventListener("unhandledrejection", onWindowUnhandledRejection);
46
47 function Probe() {
48 const { runGoalAction } = useGoalActionHandler();
49 const run = (label: string) => {
50 runGoalAction(async () => {
51 throw new Error(`${label} bridge failed`);
52 });
53 };
54 return (
55 <>
56 <button type="button" data-action="stop" onClick={() => run("stop goal")}>stop</button>
57 <button type="button" data-action="mode" onClick={() => run("switch mode")}>mode</button>
58 <button type="button" data-action="resync" onClick={() => run("background goal resync")}>resync</button>
59 </>
60 );
61 }
62
63 const rootElement = document.getElementById("root");
64 if (!rootElement) throw new Error("missing root");
65 const root = createRoot(rootElement);
66 await act(async () => {
67 root.render(<ToastProvider><Probe /></ToastProvider>);
68 });
69
70 for (const action of ["stop", "mode", "resync"]) {
71 await act(async () => {
72 document.querySelector<HTMLButtonElement>(`[data-action="${action}"]`)?.click();
73 await flushPromises();
74 });
75 }
76
77 const errors = Array.from(document.querySelectorAll(".toast--error .toast__text")).map((node) => node.textContent);
78 ok(errors.includes("stop goal bridge failed"), "rejected Stop Goal action shows an error toast");
79 ok(errors.includes("switch mode bridge failed"), "rejected mode action shows an error toast");
80 ok(errors.includes("background goal resync bridge failed"), "rejected background Goal resync shows an error toast");
81 ok(unhandled.length === 0, "handled Goal action rejections do not emit unhandledrejection");
82
83 const here = dirname(fileURLToPath(import.meta.url));
84 const appSource = readFileSync(resolve(here, "../App.tsx"), "utf8");
85 ok(
86 /runGoalAction\(\(\) => applyCollaborationMode\(collaborationMode === "plan" \? "normal" : "plan"\)\)/.test(appSource),
87 "mode shortcut routes through the rejection handler",
88 );
89 ok(
90 /runGoalAction\(async \(\) => \{[\s\S]{0,260}setControllerComposerProfileForTab\([\s\S]{0,260}propagateError: true/.test(appSource),
91 "background Goal resync routes through the rejection handler",
92 );
93 ok(appSource.includes("onClearGoal={clearGoalFromUi}"), "Composer Stop Goal routes through the rejection handler");
94 ok(appSource.includes("onSetCollaborationMode={setCollaborationModeFromUi}"), "Composer mode changes route through the rejection handler");
95 ok(/if \(model\) \{\s*await switchModel\(model\[1\]\);/.test(appSource), "/model awaits Goal restoration failures");
96 ok(
97 /await \(trimmed \? setControllerGoalForTab\(tabId, trimmed\) : clearControllerGoalForTab\(tabId\)\);\s*patchActivatedGoalForTab\(tabId, trimmed\)/.test(appSource),
98 "failed Goal bridge calls cannot patch local Goal state or user intent",
99 );
100
101 await act(async () => {
102 root.unmount();
103 });
104 process.off("unhandledRejection", onUnhandledRejection);
105 window.removeEventListener("unhandledrejection", onWindowUnhandledRejection);
106 dom.window.close();
107
108 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
109 if (failed > 0) process.exit(1);
110
110 lines Plain Text