返回 DeepSeek-Reasonix
composer-insert-commands.test.tsx
根目录 / desktop / frontend / src / __tests__ / composer-insert-commands.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { useComposerInsertCommands, type ComposerInsertCommandsInput } from "../app-runtime/useComposerInsertCommands";
6 import type { Translator } from "../lib/i18n";
7
8 const dom = new JSDOM("<div id='root'></div>");
9 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
10 const root = createRoot(document.getElementById("root")!);
11
12 const t = ((key: string) => key) as Translator;
13 const toasts: string[] = [];
14
15 function deferred<T>() {
16 let resolve!: (value: T) => void;
17 const promise = new Promise<T>((yes) => { resolve = yes; });
18 return { promise, resolve };
19 }
20
21 const terminalReads: string[] = [];
22 let terminalGate: ReturnType<typeof deferred<string>> | null = null;
23
24 const operations: ComposerInsertCommandsInput["operations"] = async (target, channel, input, execute) => {
25 const authority = { checkpoint() {}, ownsUI: () => true };
26 try {
27 const value = await execute(input, authority);
28 return { status: "completed", value };
29 } catch (error) {
30 return { status: "failed", error };
31 }
32 };
33
34 let states!: ReturnType<typeof useComposerInsertCommands>;
35 function Probe({ approval }: { approval?: { id: string; tool: string } | null }) {
36 states = useComposerInsertCommands({
37 activeTabId: "A",
38 sessionKey: "A:1",
39 approval,
40 operations,
41 t,
42 showToast: (message) => { toasts.push(message); },
43 ports: {
44 terminalOutput: async (tabId, sessionId) => {
45 terminalReads.push(`${tabId}:${sessionId}`);
46 return terminalGate ? terminalGate.promise : "last output";
47 },
48 },
49 });
50 return null;
51 }
52 const paint = (approval?: { id: string; tool: string } | null) =>
53 act(async () => root.render(<Probe approval={approval} />));
54
55 try {
56 await paint();
57 await act(async () => { states.addWorkspaceTextToComposer("hello"); });
58 assert.equal(states.composerInsertRequest?.text, "hello", "plain workspace text lands in the composer");
59 assert.equal(states.composerInsertRequest?.mode, undefined, "plain insert keeps the default append mode");
60
61 await act(async () => { states.prefillSubagentCommand("/run tests"); });
62 assert.equal(states.composerInsertRequest?.mode, "prefix", "subagent prefill uses prefix mode");
63
64 await act(async () => { states.replaceComposerInsert("A", ""); });
65 assert.equal(states.composerInsertRequest?.mode, "replace", "undo clears through a replace insert");
66
67 await act(async () => { states.addSelectedTextToComposer(" snippet "); });
68 assert.equal(states.selectedTextRequest?.text, "snippet", "selected text is trimmed before insert");
69 await act(async () => { states.addSelectedTextToComposer(" "); });
70 assert.equal(states.selectedTextRequest?.text, "snippet", "blank selections insert nothing");
71
72 await act(async () => { states.addWorkspaceCodeToComposer("src/a.ts", "const a = 1;"); });
73 assert.equal(states.selectedTextRequest?.path, "src/a.ts", "workspace code carries its path");
74
75 await act(async () => { states.handleRevisionActiveChange(true); });
76 await paint({ id: "ap-1", tool: "exit_plan_mode" });
77 await act(async () => { states.addWorkspaceTextToComposer("revise this"); });
78 assert.equal(states.activePlanRevisionInsertRequest?.text, "revise this", "plan-revision target routes plain text to the revision input");
79 assert.equal(states.composerInsertRequest?.mode, "replace", "plan-revision routing does not touch the composer");
80 await act(async () => { states.addWorkspaceCodeToComposer("src/b.ts", "code"); });
81 assert.equal(states.activePlanRevisionInsertRequest?.text?.includes("src/b.ts"), true, "code lands in the revision input as a fenced reference");
82
83 await paint({ id: "ap-2", tool: "exit_plan_mode" });
84 assert.equal(states.activePlanRevisionInsertRequest, null, "a replacement approval id invalidates the pending revision insert");
85
86 await paint(null);
87 await act(async () => { await states.addTerminalOutputToComposer("term-9"); });
88 assert.deepEqual(terminalReads, ["A:term-9"], "terminal output reads through the session port");
89 assert.equal(states.composerInsertRequest?.text?.includes("last output"), true, "terminal output is formatted into the composer");
90
91 terminalGate = deferred<string>();
92 const pending = states.addTerminalOutputToComposer("term-10");
93 await act(async () => { terminalGate!.resolve(""); await pending; });
94 assert.deepEqual(toasts, ["terminal.noOutput"], "empty terminal output reports once");
95
96 await act(async () => root.unmount());
97 console.log("composer insert commands: routing, plan-revision target, selection trimming and terminal output chains passed");
98 } finally { dom.window.close(); }
99
99 lines Plain Text