返回 DeepSeek-Reasonix
use-controller-send-fallback.test.tsx
根目录 / desktop / frontend / src / __tests__ / use-controller-send-fallback.test.tsx
1 // Run: tsx src/__tests__/use-controller-send-fallback.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React, { act } from "react";
5 import { createRoot } from "react-dom/client";
6 import type { AppBindings } from "../lib/bridge";
7 import { useController } from "../lib/useController";
8 import type { BalanceInfo, CheckpointMeta, ContextInfo, EffortInfo, HistoryMessage, JobView, Meta, TabMeta } from "../lib/types";
9
10 let passed = 0;
11 let failed = 0;
12
13 function ok(value: boolean, label: string) {
14 if (value) {
15 process.stdout.write(` PASS ${label}\n`);
16 passed += 1;
17 } else {
18 process.stdout.write(` FAIL ${label}\n`);
19 failed += 1;
20 }
21 }
22
23 function eq(actual: unknown, expected: unknown, label: string) {
24 ok(actual === expected, `${label}${actual === expected ? "" : `: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`}`);
25 }
26
27 function flushPromises(): Promise<void> {
28 return new Promise((resolve) => setTimeout(resolve, 0));
29 }
30
31 function tabMeta(overrides: Partial<TabMeta> = {}): TabMeta {
32 return {
33 id: "tab-send",
34 scope: "project",
35 workspaceRoot: "/repo/send",
36 workspaceName: "send",
37 workspacePath: "/repo/send",
38 gitBranch: "main",
39 topicId: "topic-send",
40 topicTitle: "Send",
41 label: "model-send",
42 ready: true,
43 running: false,
44 mode: "normal",
45 toolApprovalMode: "ask",
46 tokenMode: "full",
47 active: true,
48 cwd: "/repo/send",
49 ...overrides,
50 };
51 }
52
53 function metaFor(tab: TabMeta): Meta {
54 return {
55 label: tab.label,
56 ready: tab.ready,
57 startupErr: tab.startupErr,
58 eventChannel: "agent:event",
59 cwd: tab.cwd || tab.workspaceRoot,
60 workspaceRoot: tab.workspaceRoot,
61 workspaceName: tab.workspaceName,
62 workspacePath: tab.workspacePath,
63 gitBranch: tab.gitBranch,
64 autoApproveTools: false,
65 bypass: false,
66 collaborationMode: tab.collaborationMode ?? "normal",
67 toolApprovalMode: tab.toolApprovalMode ?? "ask",
68 tokenMode: tab.tokenMode ?? "full",
69 goal: "",
70 goalStatus: "stopped",
71 };
72 }
73
74 console.log("\nuse controller send fallback");
75
76 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
77 pretendToBeVisual: true,
78 url: "http://localhost/",
79 });
80 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
81 globalThis.window = dom.window as unknown as Window & typeof globalThis;
82 globalThis.document = dom.window.document;
83 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
84 globalThis.Node = dom.window.Node;
85 globalThis.HTMLElement = dom.window.HTMLElement;
86 globalThis.Event = dom.window.Event;
87 globalThis.CustomEvent = dom.window.CustomEvent;
88 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
89 globalThis.MouseEvent = dom.window.MouseEvent;
90 globalThis.localStorage = dom.window.localStorage;
91 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
92 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
93
94 const backendTab = tabMeta({ backgroundJobs: 2 });
95 const context: ContextInfo = { used: 0, window: 100, sessionTokens: 0 };
96 const effort: EffortInfo = { supported: true, current: "auto", default: "auto", levels: ["auto"] };
97 const balance: BalanceInfo = { available: false, display: "" };
98 const jobs: JobView[] = [];
99 const checkpoints: CheckpointMeta[] = [];
100 let tabsAvailable = false;
101 let submitCalls = 0;
102
103 window.runtime = {
104 EventsOn: () => () => {},
105 BrowserOpenURL: () => {},
106 };
107 window.go = {
108 main: {
109 App: {
110 ListTabs: async () => (tabsAvailable ? [backendTab] : []),
111 MetaForTab: async () => metaFor(backendTab),
112 ContextUsageForTab: async () => context,
113 EffortForTab: async () => effort,
114 BalanceForTab: async () => balance,
115 JobsForTab: async () => jobs,
116 CheckpointsForTab: async () => checkpoints,
117 HistoryForTab: async (): Promise<HistoryMessage[]> => [],
118 HistoryPageForTab: async () => ({ messages: [], startTurn: 0, endTurn: 0, totalTurns: 0, hasOlder: false }),
119 HistoryCheckpointTurnsForTab: async () => [],
120 ReplayPendingPrompts: async () => {},
121 SubmitToTab: async (tabId: string) => {
122 submitCalls += tabId === "tab-send" ? 1 : 0;
123 },
124 } as Partial<AppBindings> as AppBindings,
125 },
126 };
127
128 type Controller = ReturnType<typeof useController>;
129 let controller: Controller | undefined;
130
131 function Probe() {
132 controller = useController();
133 return null;
134 }
135
136 const rootEl = document.getElementById("root");
137 if (!rootEl) throw new Error("missing root");
138 const root = createRoot(rootEl);
139
140 await act(async () => {
141 root.render(<Probe />);
142 await flushPromises();
143 });
144 eq(controller?.activeTabId, undefined, "startup has no active tab when backend has no tabs");
145
146 tabsAvailable = true;
147 await act(async () => {
148 await controller?.send("hello from fallback");
149 await flushPromises();
150 });
151
152 eq(controller?.activeTabId, "tab-send", "send fallback activates the backend-selected tab");
153 eq(controller?.state.backgroundJobs, 2, "send fallback reconciles backend runtime metadata");
154 ok(controller?.state.items.some((item) => item.kind === "user" && item.text === "hello from fallback") ?? false, "send fallback keeps the optimistic user turn");
155 eq(submitCalls, 1, "send fallback submits to the activated tab");
156
157 await act(async () => {
158 root.unmount();
159 });
160 dom.window.close();
161
162 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
163 if (failed > 0) process.exit(1);
164
164 lines Plain Text