| 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 |