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