返回 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 { useComposerGoalCommands } from "../app-runtime/useComposerGoalCommands";
11 import { ToastProvider } from "../lib/toast";
12
13 let passed = 0;
14 let failed = 0;
15
16 function ok(value: boolean, label: string) {
17 if (value) {
18 process.stdout.write(` PASS ${label}\n`);
19 passed += 1;
20 } else {
21 process.stdout.write(` FAIL ${label}\n`);
22 failed += 1;
23 }
24 }
25
26 function flushPromises(): Promise<void> {
27 return new Promise((resolve) => setTimeout(resolve, 0));
28 }
29
30 console.log("\ngoal action bridge errors");
31
32 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
33 pretendToBeVisual: true,
34 url: "http://localhost/",
35 });
36 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
37 globalThis.window = dom.window as unknown as Window & typeof globalThis;
38 globalThis.document = dom.window.document;
39 globalThis.Event = dom.window.Event;
40 globalThis.MouseEvent = dom.window.MouseEvent;
41
42 const unhandled: unknown[] = [];
43 const onUnhandledRejection = (reason: unknown) => unhandled.push(reason);
44 const onWindowUnhandledRejection = (event: PromiseRejectionEvent) => unhandled.push(event.reason);
45 process.on("unhandledRejection", onUnhandledRejection);
46 window.addEventListener("unhandledrejection", onWindowUnhandledRejection);
47
48 function Probe() {
49 const { runGoalAction } = useGoalActionHandler();
50 const { clearGoalFromUi, setCollaborationModeFromUi } = useComposerGoalCommands({
51 applyGoal: async (goal) => { if (goal !== "") throw new Error("wrong goal capture"); throw new Error("stop goal bridge failed"); },
52 editGoal: async () => { throw new Error("edit goal bridge failed"); },
53 applyCollaborationMode: async (mode) => { if (mode !== "plan") throw new Error("wrong mode capture"); throw new Error("switch mode bridge failed"); },
54 });
55 const run = (label: string) => {
56 runGoalAction(async () => {
57 throw new Error(`${label} bridge failed`);
58 });
59 };
60 return (
61 <>
62 <button type="button" data-action="stop" onClick={clearGoalFromUi}>stop</button>
63 <button type="button" data-action="mode" onClick={() => setCollaborationModeFromUi("plan")}>mode</button>
64 <button type="button" data-action="resync" onClick={() => run("background goal resync")}>resync</button>
65 </>
66 );
67 }
68
69 const rootElement = document.getElementById("root");
70 if (!rootElement) throw new Error("missing root");
71 const root = createRoot(rootElement);
72 await act(async () => {
73 root.render(<ToastProvider><Probe /></ToastProvider>);
74 });
75
76 for (const action of ["stop", "mode", "resync"]) {
77 await act(async () => {
78 document.querySelector<HTMLButtonElement>(`[data-action="${action}"]`)?.click();
79 await flushPromises();
80 });
81 }
82
83 const errors = Array.from(document.querySelectorAll(".toast--error .toast__text")).map((node) => node.textContent);
84 ok(errors.includes("stop goal bridge failed"), "rejected Stop Goal action shows an error toast");
85 ok(errors.includes("switch mode bridge failed"), "rejected mode action shows an error toast");
86 ok(errors.includes("background goal resync bridge failed"), "rejected background Goal resync shows an error toast");
87 ok(unhandled.length === 0, "handled Goal action rejections do not emit unhandledrejection");
88
89 const here = dirname(fileURLToPath(import.meta.url));
90 const appSource = readFileSync(resolve(here, "../app-runtime/useAppSessionComposition.ts"), "utf8");
91 ok(
92 /runGoalAction\(\(\) => applyCollaborationMode\(collaborationMode === "plan" \? "normal" : "plan"\)\)/.test(appSource),
93 "mode shortcut routes through the rejection handler",
94 );
95 // controller-profile-lifecycle.test.tsx drives the production restoration effect
96 // and verifies one error report, alongside the direct/awaited model reject contract.
97 ok(errors.filter(error => error === "stop goal bridge failed").length === 1, "production Composer Stop Goal adapter presents the failure exactly once");
98 ok(errors.filter(error => error === "switch mode bridge failed").length === 1, "production Composer mode adapter presents the failure exactly once");
99 // session-submission-lifecycle.test.tsx rejects real Goal activation and checks
100 // zero profile/intent patches or submit/undo side effects.
101
102 await act(async () => {
103 root.unmount();
104 });
105 process.off("unhandledRejection", onUnhandledRejection);
106 window.removeEventListener("unhandledrejection", onWindowUnhandledRejection);
107 dom.window.close();
108
109 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
110 if (failed > 0) process.exit(1);
111
111 lines Plain Text