@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- package/dist/index.js +0 -26
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { render, cleanup, logInk } from "../../../../lib/test-utils.js";
|
|
5
|
+
import { sleep } from "../../../../lib/utils.js";
|
|
6
|
+
import { BashRenderer, type BashRendererProps } from "../../modes/bash-renderer.js";
|
|
7
|
+
import { destroyPersistentShell } from "../../persistent-shell.js";
|
|
8
|
+
|
|
9
|
+
// Test wrapper component to manage state
|
|
10
|
+
function BashRendererWrapper(
|
|
11
|
+
props: Omit<BashRendererProps, "state" | "setState"> & {
|
|
12
|
+
initialState?: BashRendererProps["state"];
|
|
13
|
+
},
|
|
14
|
+
) {
|
|
15
|
+
const [state, setState] = useState<BashRendererProps["state"]>(
|
|
16
|
+
props.initialState || {
|
|
17
|
+
submittedCommand: null,
|
|
18
|
+
runId: 0,
|
|
19
|
+
output: "",
|
|
20
|
+
isRunning: false,
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return <BashRenderer {...props} state={state} setState={setState} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe.sequential("BashRenderer", () => {
|
|
28
|
+
const mockSetState = vi.fn();
|
|
29
|
+
const mockOnCommandComplete = vi.fn();
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
// Clean up any existing shell instance before each test
|
|
34
|
+
destroyPersistentShell();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
cleanup();
|
|
39
|
+
// Clean up shell instance after each test
|
|
40
|
+
destroyPersistentShell();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should render with empty state", () => {
|
|
44
|
+
const instance = render(
|
|
45
|
+
<BashRenderer
|
|
46
|
+
state={{
|
|
47
|
+
submittedCommand: null,
|
|
48
|
+
runId: 0,
|
|
49
|
+
output: "",
|
|
50
|
+
isRunning: false,
|
|
51
|
+
}}
|
|
52
|
+
setState={mockSetState}
|
|
53
|
+
onCommandComplete={mockOnCommandComplete}
|
|
54
|
+
commandHistory={[]}
|
|
55
|
+
currentPrompt=""
|
|
56
|
+
availableHeight={20}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
logInk(instance);
|
|
61
|
+
|
|
62
|
+
const output = instance.frames.join("");
|
|
63
|
+
expect(output).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should render command history", () => {
|
|
67
|
+
const commandHistory = [
|
|
68
|
+
{ command: "echo hello", output: "hello" },
|
|
69
|
+
{ command: "ls", output: "file1.txt\nfile2.txt" },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const instance = render(
|
|
73
|
+
<BashRenderer
|
|
74
|
+
state={{
|
|
75
|
+
submittedCommand: null,
|
|
76
|
+
runId: 0,
|
|
77
|
+
output: "",
|
|
78
|
+
isRunning: false,
|
|
79
|
+
}}
|
|
80
|
+
setState={mockSetState}
|
|
81
|
+
onCommandComplete={mockOnCommandComplete}
|
|
82
|
+
commandHistory={commandHistory}
|
|
83
|
+
currentPrompt=""
|
|
84
|
+
availableHeight={20}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
logInk(instance);
|
|
89
|
+
|
|
90
|
+
const output = instance.frames.join("");
|
|
91
|
+
expect(output).toContain("echo hello");
|
|
92
|
+
expect(output).toContain("ls");
|
|
93
|
+
expect(output).toContain("hello");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should render submitted command", () => {
|
|
97
|
+
const instance = render(
|
|
98
|
+
<BashRenderer
|
|
99
|
+
state={{
|
|
100
|
+
submittedCommand: "echo test",
|
|
101
|
+
runId: 1,
|
|
102
|
+
output: "",
|
|
103
|
+
isRunning: true,
|
|
104
|
+
}}
|
|
105
|
+
setState={mockSetState}
|
|
106
|
+
onCommandComplete={mockOnCommandComplete}
|
|
107
|
+
commandHistory={[]}
|
|
108
|
+
currentPrompt=""
|
|
109
|
+
availableHeight={20}
|
|
110
|
+
/>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
logInk(instance);
|
|
114
|
+
|
|
115
|
+
const output = instance.frames.join("");
|
|
116
|
+
expect(output).toContain("echo test");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should execute command when submittedCommand is set", async () => {
|
|
120
|
+
render(
|
|
121
|
+
<BashRendererWrapper
|
|
122
|
+
initialState={{
|
|
123
|
+
submittedCommand: 'echo "output line 1\noutput line 2"',
|
|
124
|
+
runId: 1,
|
|
125
|
+
output: "",
|
|
126
|
+
isRunning: false,
|
|
127
|
+
}}
|
|
128
|
+
onCommandComplete={mockOnCommandComplete}
|
|
129
|
+
commandHistory={[]}
|
|
130
|
+
currentPrompt=""
|
|
131
|
+
availableHeight={20}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Wait for async operations
|
|
136
|
+
await sleep(500);
|
|
137
|
+
|
|
138
|
+
// Command should have been executed (verified by behavior, not mocks)
|
|
139
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should not execute command if normalized is empty", async () => {
|
|
143
|
+
render(
|
|
144
|
+
<BashRenderer
|
|
145
|
+
state={{
|
|
146
|
+
submittedCommand: " ",
|
|
147
|
+
runId: 1,
|
|
148
|
+
output: "",
|
|
149
|
+
isRunning: false,
|
|
150
|
+
}}
|
|
151
|
+
setState={mockSetState}
|
|
152
|
+
onCommandComplete={mockOnCommandComplete}
|
|
153
|
+
commandHistory={[]}
|
|
154
|
+
currentPrompt=""
|
|
155
|
+
availableHeight={20}
|
|
156
|
+
/>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await sleep(100);
|
|
160
|
+
|
|
161
|
+
// Empty command should not trigger execution
|
|
162
|
+
expect(mockOnCommandComplete).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should execute command when runId is set", async () => {
|
|
166
|
+
render(
|
|
167
|
+
<BashRenderer
|
|
168
|
+
state={{
|
|
169
|
+
submittedCommand: "echo test",
|
|
170
|
+
runId: 1,
|
|
171
|
+
output: "",
|
|
172
|
+
isRunning: false,
|
|
173
|
+
}}
|
|
174
|
+
setState={mockSetState}
|
|
175
|
+
onCommandComplete={mockOnCommandComplete}
|
|
176
|
+
commandHistory={[]}
|
|
177
|
+
currentPrompt=""
|
|
178
|
+
availableHeight={20}
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await sleep(500);
|
|
183
|
+
|
|
184
|
+
// The component should execute the command when runId is set
|
|
185
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should not re-execute command with same runId", async () => {
|
|
189
|
+
mockOnCommandComplete.mockClear();
|
|
190
|
+
|
|
191
|
+
// Use a wrapper component that maintains state internally
|
|
192
|
+
// This ensures the same component instance persists, so lastRunIdRef is preserved
|
|
193
|
+
const TestWrapper = () => {
|
|
194
|
+
const [state, setState] = useState<BashRendererProps["state"]>({
|
|
195
|
+
submittedCommand: "echo test1",
|
|
196
|
+
runId: 1,
|
|
197
|
+
output: "",
|
|
198
|
+
isRunning: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// After first command completes, update output without changing runId
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (mockOnCommandComplete.mock.calls.length === 1 && state.output === "") {
|
|
204
|
+
// Simulate parent updating output after command completes
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
setState((prev) => ({ ...prev, output: "some output" }));
|
|
207
|
+
}, 100);
|
|
208
|
+
}
|
|
209
|
+
}, [state.output]);
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<BashRenderer
|
|
213
|
+
state={state}
|
|
214
|
+
setState={setState}
|
|
215
|
+
onCommandComplete={mockOnCommandComplete}
|
|
216
|
+
commandHistory={[]}
|
|
217
|
+
currentPrompt=""
|
|
218
|
+
availableHeight={20}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
render(<TestWrapper />);
|
|
224
|
+
|
|
225
|
+
// Wait for first execution to complete
|
|
226
|
+
await sleep(600);
|
|
227
|
+
|
|
228
|
+
// Verify command was executed once
|
|
229
|
+
expect(mockOnCommandComplete).toHaveBeenCalledTimes(1);
|
|
230
|
+
const callCountAfterFirstExecution = mockOnCommandComplete.mock.calls.length;
|
|
231
|
+
|
|
232
|
+
// Wait for state update to trigger re-render (output changes but runId stays same)
|
|
233
|
+
await sleep(200);
|
|
234
|
+
|
|
235
|
+
// Wait a bit more to ensure useEffect has run and checked the runId
|
|
236
|
+
await sleep(300);
|
|
237
|
+
|
|
238
|
+
// onCommandComplete should NOT be called again because runId hasn't changed
|
|
239
|
+
// The check `if (runId === lastRunIdRef.current) return` should prevent re-execution
|
|
240
|
+
expect(mockOnCommandComplete).toHaveBeenCalledTimes(callCountAfterFirstExecution);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should execute command when runId changes", async () => {
|
|
244
|
+
const { rerender } = render(
|
|
245
|
+
<BashRenderer
|
|
246
|
+
state={{
|
|
247
|
+
submittedCommand: "echo test1",
|
|
248
|
+
runId: 1,
|
|
249
|
+
output: "",
|
|
250
|
+
isRunning: false,
|
|
251
|
+
}}
|
|
252
|
+
setState={mockSetState}
|
|
253
|
+
onCommandComplete={mockOnCommandComplete}
|
|
254
|
+
commandHistory={[]}
|
|
255
|
+
currentPrompt=""
|
|
256
|
+
availableHeight={20}
|
|
257
|
+
/>,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
await sleep(300);
|
|
261
|
+
|
|
262
|
+
// Clear previous calls
|
|
263
|
+
mockOnCommandComplete.mockClear();
|
|
264
|
+
|
|
265
|
+
// Re-render with different runId
|
|
266
|
+
rerender(
|
|
267
|
+
<BashRenderer
|
|
268
|
+
state={{
|
|
269
|
+
submittedCommand: "echo test2",
|
|
270
|
+
runId: 2,
|
|
271
|
+
output: "",
|
|
272
|
+
isRunning: false,
|
|
273
|
+
}}
|
|
274
|
+
setState={mockSetState}
|
|
275
|
+
onCommandComplete={mockOnCommandComplete}
|
|
276
|
+
commandHistory={[]}
|
|
277
|
+
currentPrompt=""
|
|
278
|
+
availableHeight={20}
|
|
279
|
+
/>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await sleep(500);
|
|
283
|
+
|
|
284
|
+
// Should have called onCommandComplete again with new command
|
|
285
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should update output as stream chunks arrive", async () => {
|
|
289
|
+
const instance = render(
|
|
290
|
+
<BashRendererWrapper
|
|
291
|
+
initialState={{
|
|
292
|
+
submittedCommand: 'echo "chunk1\nchunk2"',
|
|
293
|
+
runId: 1,
|
|
294
|
+
output: "",
|
|
295
|
+
isRunning: false,
|
|
296
|
+
}}
|
|
297
|
+
onCommandComplete={mockOnCommandComplete}
|
|
298
|
+
commandHistory={[]}
|
|
299
|
+
currentPrompt=""
|
|
300
|
+
availableHeight={20}
|
|
301
|
+
/>,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await sleep(500);
|
|
305
|
+
|
|
306
|
+
// Output should be updated as chunks arrive
|
|
307
|
+
const output = instance.frames.join("");
|
|
308
|
+
expect(output).toBeDefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should strip dangerous sequences from output", async () => {
|
|
312
|
+
// Use a real command - stripDangerousSequences is called internally
|
|
313
|
+
render(
|
|
314
|
+
<BashRendererWrapper
|
|
315
|
+
initialState={{
|
|
316
|
+
submittedCommand: "echo test",
|
|
317
|
+
runId: 1,
|
|
318
|
+
output: "",
|
|
319
|
+
isRunning: false,
|
|
320
|
+
}}
|
|
321
|
+
onCommandComplete={mockOnCommandComplete}
|
|
322
|
+
commandHistory={[]}
|
|
323
|
+
currentPrompt=""
|
|
324
|
+
availableHeight={20}
|
|
325
|
+
/>,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
await sleep(500);
|
|
329
|
+
|
|
330
|
+
// stripDangerousSequences is called internally, we verify behavior
|
|
331
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should handle command execution error gracefully", async () => {
|
|
335
|
+
// Use a command that will fail (non-existent command)
|
|
336
|
+
render(
|
|
337
|
+
<BashRendererWrapper
|
|
338
|
+
initialState={{
|
|
339
|
+
submittedCommand: "nonexistentcommand12345",
|
|
340
|
+
runId: 1,
|
|
341
|
+
output: "",
|
|
342
|
+
isRunning: false,
|
|
343
|
+
}}
|
|
344
|
+
onCommandComplete={mockOnCommandComplete}
|
|
345
|
+
commandHistory={[]}
|
|
346
|
+
currentPrompt=""
|
|
347
|
+
availableHeight={20}
|
|
348
|
+
/>,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
await sleep(500);
|
|
352
|
+
|
|
353
|
+
// Error handling should call onCommandComplete
|
|
354
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should call onCommandComplete with output when command succeeds", async () => {
|
|
358
|
+
render(
|
|
359
|
+
<BashRendererWrapper
|
|
360
|
+
initialState={{
|
|
361
|
+
submittedCommand: 'echo "command output"',
|
|
362
|
+
runId: 1,
|
|
363
|
+
output: "",
|
|
364
|
+
isRunning: false,
|
|
365
|
+
}}
|
|
366
|
+
onCommandComplete={mockOnCommandComplete}
|
|
367
|
+
commandHistory={[]}
|
|
368
|
+
currentPrompt=""
|
|
369
|
+
availableHeight={20}
|
|
370
|
+
/>,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
await sleep(500);
|
|
374
|
+
|
|
375
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
376
|
+
// Verify it was called with output (not empty string)
|
|
377
|
+
const calls = mockOnCommandComplete.mock.calls;
|
|
378
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
379
|
+
if (calls.length > 0 && calls[0][0]) {
|
|
380
|
+
expect(calls[0][0].length).toBeGreaterThan(0);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should set isRunning to true when command starts", async () => {
|
|
385
|
+
render(
|
|
386
|
+
<BashRenderer
|
|
387
|
+
state={{
|
|
388
|
+
submittedCommand: "echo test",
|
|
389
|
+
runId: 1,
|
|
390
|
+
output: "",
|
|
391
|
+
isRunning: false,
|
|
392
|
+
}}
|
|
393
|
+
setState={mockSetState}
|
|
394
|
+
onCommandComplete={mockOnCommandComplete}
|
|
395
|
+
commandHistory={[]}
|
|
396
|
+
currentPrompt=""
|
|
397
|
+
availableHeight={20}
|
|
398
|
+
/>,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
await sleep(50);
|
|
402
|
+
|
|
403
|
+
// setState is called with a function, so we check if it was called
|
|
404
|
+
expect(mockSetState).toHaveBeenCalled();
|
|
405
|
+
// Verify it's called with a function that returns isRunning: true
|
|
406
|
+
const setStateCall = mockSetState.mock.calls.find((call) => {
|
|
407
|
+
if (typeof call[0] === "function") {
|
|
408
|
+
const result = call[0]({ isRunning: false });
|
|
409
|
+
return result.isRunning === true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
});
|
|
413
|
+
expect(setStateCall).toBeDefined();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should set isRunning to false when command completes", async () => {
|
|
417
|
+
render(
|
|
418
|
+
<BashRenderer
|
|
419
|
+
state={{
|
|
420
|
+
submittedCommand: "echo test",
|
|
421
|
+
runId: 1,
|
|
422
|
+
output: "",
|
|
423
|
+
isRunning: false,
|
|
424
|
+
}}
|
|
425
|
+
setState={mockSetState}
|
|
426
|
+
onCommandComplete={mockOnCommandComplete}
|
|
427
|
+
commandHistory={[]}
|
|
428
|
+
currentPrompt=""
|
|
429
|
+
availableHeight={20}
|
|
430
|
+
/>,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
await sleep(500);
|
|
434
|
+
|
|
435
|
+
// setState is called with a function, verify it sets isRunning to false
|
|
436
|
+
expect(mockSetState).toHaveBeenCalled();
|
|
437
|
+
const setStateCall = mockSetState.mock.calls.find((call) => {
|
|
438
|
+
if (typeof call[0] === "function") {
|
|
439
|
+
const result = call[0]({ isRunning: true });
|
|
440
|
+
return result.isRunning === false;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
});
|
|
444
|
+
expect(setStateCall).toBeDefined();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("should limit history display when submittedCommand exists", () => {
|
|
448
|
+
const commandHistory = [
|
|
449
|
+
{ command: "cmd1", output: "out1" },
|
|
450
|
+
{ command: "cmd2", output: "out2" },
|
|
451
|
+
{ command: "cmd3", output: "out3" },
|
|
452
|
+
{ command: "cmd4", output: "out4" },
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
const instance = render(
|
|
456
|
+
<BashRenderer
|
|
457
|
+
state={{
|
|
458
|
+
submittedCommand: "current",
|
|
459
|
+
runId: 1,
|
|
460
|
+
output: "",
|
|
461
|
+
isRunning: false,
|
|
462
|
+
}}
|
|
463
|
+
setState={mockSetState}
|
|
464
|
+
onCommandComplete={mockOnCommandComplete}
|
|
465
|
+
commandHistory={commandHistory}
|
|
466
|
+
currentPrompt=""
|
|
467
|
+
availableHeight={20}
|
|
468
|
+
/>,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
logInk(instance);
|
|
472
|
+
|
|
473
|
+
const output = instance.frames.join("");
|
|
474
|
+
// Should show only last 2 history items when submittedCommand exists
|
|
475
|
+
expect(output).toContain("cmd3");
|
|
476
|
+
expect(output).toContain("cmd4");
|
|
477
|
+
expect(output).not.toContain("cmd1");
|
|
478
|
+
expect(output).not.toContain("cmd2");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("should show more history when no submittedCommand", () => {
|
|
482
|
+
const commandHistory = [
|
|
483
|
+
{ command: "cmd1", output: "out1" },
|
|
484
|
+
{ command: "cmd2", output: "out2" },
|
|
485
|
+
{ command: "cmd3", output: "out3" },
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
const instance = render(
|
|
489
|
+
<BashRenderer
|
|
490
|
+
state={{
|
|
491
|
+
submittedCommand: null,
|
|
492
|
+
runId: 0,
|
|
493
|
+
output: "",
|
|
494
|
+
isRunning: false,
|
|
495
|
+
}}
|
|
496
|
+
setState={mockSetState}
|
|
497
|
+
onCommandComplete={mockOnCommandComplete}
|
|
498
|
+
commandHistory={commandHistory}
|
|
499
|
+
currentPrompt=""
|
|
500
|
+
availableHeight={20}
|
|
501
|
+
/>,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
logInk(instance);
|
|
505
|
+
|
|
506
|
+
const output = instance.frames.join("");
|
|
507
|
+
// Should show last 3 history items when no submittedCommand
|
|
508
|
+
expect(output).toContain("cmd1");
|
|
509
|
+
expect(output).toContain("cmd2");
|
|
510
|
+
expect(output).toContain("cmd3");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("should calculate maxLinesPerCommand based on available height", () => {
|
|
514
|
+
const commandHistory = [
|
|
515
|
+
{ command: "cmd1", output: "out1" },
|
|
516
|
+
{ command: "cmd2", output: "out2" },
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
const instance = render(
|
|
520
|
+
<BashRenderer
|
|
521
|
+
state={{
|
|
522
|
+
submittedCommand: "current",
|
|
523
|
+
runId: 1,
|
|
524
|
+
output: "",
|
|
525
|
+
isRunning: false,
|
|
526
|
+
}}
|
|
527
|
+
setState={mockSetState}
|
|
528
|
+
onCommandComplete={mockOnCommandComplete}
|
|
529
|
+
commandHistory={commandHistory}
|
|
530
|
+
currentPrompt=""
|
|
531
|
+
availableHeight={10}
|
|
532
|
+
/>,
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
logInk(instance);
|
|
536
|
+
|
|
537
|
+
const output = instance.frames.join("");
|
|
538
|
+
expect(output).toBeDefined();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("should enable truncation when height is limited", () => {
|
|
542
|
+
const commandHistory = [{ command: "cmd1", output: "line1\nline2\nline3\nline4\nline5" }];
|
|
543
|
+
|
|
544
|
+
const instance = render(
|
|
545
|
+
<BashRenderer
|
|
546
|
+
state={{
|
|
547
|
+
submittedCommand: null,
|
|
548
|
+
runId: 0,
|
|
549
|
+
output: "",
|
|
550
|
+
isRunning: false,
|
|
551
|
+
}}
|
|
552
|
+
setState={mockSetState}
|
|
553
|
+
onCommandComplete={mockOnCommandComplete}
|
|
554
|
+
commandHistory={commandHistory}
|
|
555
|
+
currentPrompt=""
|
|
556
|
+
availableHeight={5}
|
|
557
|
+
/>,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
logInk(instance);
|
|
561
|
+
|
|
562
|
+
const output = instance.frames.join("");
|
|
563
|
+
expect(output).toBeDefined();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("should enable truncation when typing", () => {
|
|
567
|
+
const commandHistory = [{ command: "cmd1", output: "line1\nline2\nline3\nline4\nline5" }];
|
|
568
|
+
|
|
569
|
+
const instance = render(
|
|
570
|
+
<BashRenderer
|
|
571
|
+
state={{
|
|
572
|
+
submittedCommand: null,
|
|
573
|
+
runId: 0,
|
|
574
|
+
output: "",
|
|
575
|
+
isRunning: false,
|
|
576
|
+
}}
|
|
577
|
+
setState={mockSetState}
|
|
578
|
+
onCommandComplete={mockOnCommandComplete}
|
|
579
|
+
commandHistory={commandHistory}
|
|
580
|
+
currentPrompt="typing..."
|
|
581
|
+
availableHeight={20}
|
|
582
|
+
/>,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
logInk(instance);
|
|
586
|
+
|
|
587
|
+
const output = instance.frames.join("");
|
|
588
|
+
expect(output).toBeDefined();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("should enable truncation when multiple commands exist", () => {
|
|
592
|
+
const commandHistory = [
|
|
593
|
+
{ command: "cmd1", output: "out1" },
|
|
594
|
+
{ command: "cmd2", output: "out2" },
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
const instance = render(
|
|
598
|
+
<BashRenderer
|
|
599
|
+
state={{
|
|
600
|
+
submittedCommand: "cmd3",
|
|
601
|
+
runId: 1,
|
|
602
|
+
output: "",
|
|
603
|
+
isRunning: false,
|
|
604
|
+
}}
|
|
605
|
+
setState={mockSetState}
|
|
606
|
+
onCommandComplete={mockOnCommandComplete}
|
|
607
|
+
commandHistory={commandHistory}
|
|
608
|
+
currentPrompt=""
|
|
609
|
+
availableHeight={20}
|
|
610
|
+
/>,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
logInk(instance);
|
|
614
|
+
|
|
615
|
+
const output = instance.frames.join("");
|
|
616
|
+
expect(output).toBeDefined();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("should handle empty command history", () => {
|
|
620
|
+
const instance = render(
|
|
621
|
+
<BashRenderer
|
|
622
|
+
state={{
|
|
623
|
+
submittedCommand: "test",
|
|
624
|
+
runId: 1,
|
|
625
|
+
output: "",
|
|
626
|
+
isRunning: false,
|
|
627
|
+
}}
|
|
628
|
+
setState={mockSetState}
|
|
629
|
+
onCommandComplete={mockOnCommandComplete}
|
|
630
|
+
commandHistory={[]}
|
|
631
|
+
currentPrompt=""
|
|
632
|
+
availableHeight={20}
|
|
633
|
+
/>,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
logInk(instance);
|
|
637
|
+
|
|
638
|
+
const output = instance.frames.join("");
|
|
639
|
+
expect(output).toContain("test");
|
|
640
|
+
expect(output).toBeDefined();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("should trim submittedCommand before executing", async () => {
|
|
644
|
+
render(
|
|
645
|
+
<BashRendererWrapper
|
|
646
|
+
initialState={{
|
|
647
|
+
submittedCommand: " echo trimmed-test ",
|
|
648
|
+
runId: 1,
|
|
649
|
+
output: "",
|
|
650
|
+
isRunning: false,
|
|
651
|
+
}}
|
|
652
|
+
onCommandComplete={mockOnCommandComplete}
|
|
653
|
+
commandHistory={[]}
|
|
654
|
+
currentPrompt=""
|
|
655
|
+
availableHeight={20}
|
|
656
|
+
/>,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
await sleep(500);
|
|
660
|
+
|
|
661
|
+
// Command should be executed with trimmed value
|
|
662
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("should handle very small availableHeight", () => {
|
|
666
|
+
const instance = render(
|
|
667
|
+
<BashRenderer
|
|
668
|
+
state={{
|
|
669
|
+
submittedCommand: "test",
|
|
670
|
+
runId: 1,
|
|
671
|
+
output: "",
|
|
672
|
+
isRunning: false,
|
|
673
|
+
}}
|
|
674
|
+
setState={mockSetState}
|
|
675
|
+
onCommandComplete={mockOnCommandComplete}
|
|
676
|
+
commandHistory={[]}
|
|
677
|
+
currentPrompt=""
|
|
678
|
+
availableHeight={1}
|
|
679
|
+
/>,
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
logInk(instance);
|
|
683
|
+
|
|
684
|
+
const output = instance.frames.join("");
|
|
685
|
+
expect(output).toBeDefined();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("should handle multiple output chunks", async () => {
|
|
689
|
+
render(
|
|
690
|
+
<BashRendererWrapper
|
|
691
|
+
initialState={{
|
|
692
|
+
submittedCommand: 'echo "chunk1chunk2chunk3"',
|
|
693
|
+
runId: 1,
|
|
694
|
+
output: "",
|
|
695
|
+
isRunning: false,
|
|
696
|
+
}}
|
|
697
|
+
onCommandComplete={mockOnCommandComplete}
|
|
698
|
+
commandHistory={[]}
|
|
699
|
+
currentPrompt=""
|
|
700
|
+
availableHeight={20}
|
|
701
|
+
/>,
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
await sleep(500);
|
|
705
|
+
|
|
706
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
707
|
+
const calls = mockOnCommandComplete.mock.calls;
|
|
708
|
+
if (calls.length > 0 && calls[0][0]) {
|
|
709
|
+
expect(calls[0][0]).toContain("chunk");
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("should handle stderr output", async () => {
|
|
714
|
+
// Use a command that produces stderr output
|
|
715
|
+
render(
|
|
716
|
+
<BashRendererWrapper
|
|
717
|
+
initialState={{
|
|
718
|
+
submittedCommand: 'echo "error message" >&2',
|
|
719
|
+
runId: 1,
|
|
720
|
+
output: "",
|
|
721
|
+
isRunning: false,
|
|
722
|
+
}}
|
|
723
|
+
onCommandComplete={mockOnCommandComplete}
|
|
724
|
+
commandHistory={[]}
|
|
725
|
+
currentPrompt=""
|
|
726
|
+
availableHeight={20}
|
|
727
|
+
/>,
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
await sleep(500);
|
|
731
|
+
|
|
732
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("should handle mixed stdout and stderr", async () => {
|
|
736
|
+
// Use a command that produces both stdout and stderr
|
|
737
|
+
render(
|
|
738
|
+
<BashRendererWrapper
|
|
739
|
+
initialState={{
|
|
740
|
+
submittedCommand: 'echo "stdout" && echo "stderr" >&2',
|
|
741
|
+
runId: 1,
|
|
742
|
+
output: "",
|
|
743
|
+
isRunning: false,
|
|
744
|
+
}}
|
|
745
|
+
onCommandComplete={mockOnCommandComplete}
|
|
746
|
+
commandHistory={[]}
|
|
747
|
+
currentPrompt=""
|
|
748
|
+
availableHeight={20}
|
|
749
|
+
/>,
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
await sleep(500);
|
|
753
|
+
|
|
754
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("should handle component unmount during command execution", async () => {
|
|
758
|
+
const instance = render(
|
|
759
|
+
<BashRendererWrapper
|
|
760
|
+
initialState={{
|
|
761
|
+
submittedCommand: "sleep 0.1 && echo test",
|
|
762
|
+
runId: 1,
|
|
763
|
+
output: "",
|
|
764
|
+
isRunning: false,
|
|
765
|
+
}}
|
|
766
|
+
onCommandComplete={mockOnCommandComplete}
|
|
767
|
+
commandHistory={[]}
|
|
768
|
+
currentPrompt=""
|
|
769
|
+
availableHeight={20}
|
|
770
|
+
/>,
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
logInk(instance);
|
|
774
|
+
|
|
775
|
+
// Unmount before command completes
|
|
776
|
+
cleanup();
|
|
777
|
+
|
|
778
|
+
// Should not crash
|
|
779
|
+
await sleep(200);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("should handle new command submission while previous is running", async () => {
|
|
783
|
+
const { rerender } = render(
|
|
784
|
+
<BashRenderer
|
|
785
|
+
state={{
|
|
786
|
+
submittedCommand: "echo command1",
|
|
787
|
+
runId: 1,
|
|
788
|
+
output: "",
|
|
789
|
+
isRunning: false,
|
|
790
|
+
}}
|
|
791
|
+
setState={mockSetState}
|
|
792
|
+
onCommandComplete={mockOnCommandComplete}
|
|
793
|
+
commandHistory={[]}
|
|
794
|
+
currentPrompt=""
|
|
795
|
+
availableHeight={20}
|
|
796
|
+
/>,
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
await sleep(50);
|
|
800
|
+
|
|
801
|
+
// Submit new command before first completes
|
|
802
|
+
rerender(
|
|
803
|
+
<BashRenderer
|
|
804
|
+
state={{
|
|
805
|
+
submittedCommand: "echo command2",
|
|
806
|
+
runId: 2,
|
|
807
|
+
output: "",
|
|
808
|
+
isRunning: false,
|
|
809
|
+
}}
|
|
810
|
+
setState={mockSetState}
|
|
811
|
+
onCommandComplete={mockOnCommandComplete}
|
|
812
|
+
commandHistory={[]}
|
|
813
|
+
currentPrompt=""
|
|
814
|
+
availableHeight={20}
|
|
815
|
+
/>,
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
await sleep(500);
|
|
819
|
+
|
|
820
|
+
// Should handle both commands
|
|
821
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it("should handle many commands with limited height", () => {
|
|
825
|
+
const commandHistory = Array(10)
|
|
826
|
+
.fill(0)
|
|
827
|
+
.map((_, i) => ({
|
|
828
|
+
command: `cmd${i}`,
|
|
829
|
+
output: `out${i}`,
|
|
830
|
+
}));
|
|
831
|
+
|
|
832
|
+
const instance = render(
|
|
833
|
+
<BashRenderer
|
|
834
|
+
state={{
|
|
835
|
+
submittedCommand: "current",
|
|
836
|
+
runId: 1,
|
|
837
|
+
output: "",
|
|
838
|
+
isRunning: false,
|
|
839
|
+
}}
|
|
840
|
+
setState={mockSetState}
|
|
841
|
+
onCommandComplete={mockOnCommandComplete}
|
|
842
|
+
commandHistory={commandHistory}
|
|
843
|
+
currentPrompt=""
|
|
844
|
+
availableHeight={15}
|
|
845
|
+
/>,
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
logInk(instance);
|
|
849
|
+
|
|
850
|
+
const output = instance.frames.join("");
|
|
851
|
+
expect(output).toBeDefined();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it("should call onCommandComplete with empty string on error", async () => {
|
|
855
|
+
// Use a command that will fail (non-existent command)
|
|
856
|
+
render(
|
|
857
|
+
<BashRendererWrapper
|
|
858
|
+
initialState={{
|
|
859
|
+
submittedCommand: "nonexistentcommand12345",
|
|
860
|
+
runId: 1,
|
|
861
|
+
output: "",
|
|
862
|
+
isRunning: false,
|
|
863
|
+
}}
|
|
864
|
+
onCommandComplete={mockOnCommandComplete}
|
|
865
|
+
commandHistory={[]}
|
|
866
|
+
currentPrompt=""
|
|
867
|
+
availableHeight={20}
|
|
868
|
+
/>,
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
await sleep(500);
|
|
872
|
+
|
|
873
|
+
// Error handling should call onCommandComplete
|
|
874
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("should reset output when new command starts", async () => {
|
|
878
|
+
const { rerender } = render(
|
|
879
|
+
<BashRenderer
|
|
880
|
+
state={{
|
|
881
|
+
submittedCommand: "echo command1",
|
|
882
|
+
runId: 1,
|
|
883
|
+
output: "old output",
|
|
884
|
+
isRunning: false,
|
|
885
|
+
}}
|
|
886
|
+
setState={mockSetState}
|
|
887
|
+
onCommandComplete={mockOnCommandComplete}
|
|
888
|
+
commandHistory={[]}
|
|
889
|
+
currentPrompt=""
|
|
890
|
+
availableHeight={20}
|
|
891
|
+
/>,
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
await sleep(300);
|
|
895
|
+
|
|
896
|
+
// Clear previous calls
|
|
897
|
+
mockSetState.mockClear();
|
|
898
|
+
|
|
899
|
+
// Submit new command with different runId
|
|
900
|
+
rerender(
|
|
901
|
+
<BashRenderer
|
|
902
|
+
state={{
|
|
903
|
+
submittedCommand: "echo command2",
|
|
904
|
+
runId: 2,
|
|
905
|
+
output: "",
|
|
906
|
+
isRunning: false,
|
|
907
|
+
}}
|
|
908
|
+
setState={mockSetState}
|
|
909
|
+
onCommandComplete={mockOnCommandComplete}
|
|
910
|
+
commandHistory={[]}
|
|
911
|
+
currentPrompt=""
|
|
912
|
+
availableHeight={20}
|
|
913
|
+
/>,
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
await sleep(500);
|
|
917
|
+
|
|
918
|
+
// Output should be reset for new command (setState is called with isRunning: true first)
|
|
919
|
+
// The output reset happens in the useEffect via setOutput('')
|
|
920
|
+
expect(mockSetState).toHaveBeenCalled();
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("should handle command with no output", async () => {
|
|
924
|
+
// Use a command that produces no output (like true or : command)
|
|
925
|
+
render(
|
|
926
|
+
<BashRenderer
|
|
927
|
+
state={{
|
|
928
|
+
submittedCommand: "true",
|
|
929
|
+
runId: 1,
|
|
930
|
+
output: "",
|
|
931
|
+
isRunning: false,
|
|
932
|
+
}}
|
|
933
|
+
setState={mockSetState}
|
|
934
|
+
onCommandComplete={mockOnCommandComplete}
|
|
935
|
+
commandHistory={[]}
|
|
936
|
+
currentPrompt=""
|
|
937
|
+
availableHeight={20}
|
|
938
|
+
/>,
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
await sleep(500);
|
|
942
|
+
|
|
943
|
+
// When stream has no chunks, commandOutput remains empty string
|
|
944
|
+
// onCommandComplete should be called with empty string to signal completion
|
|
945
|
+
expect(mockOnCommandComplete).toHaveBeenCalledWith("");
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it("should handle very long command output", async () => {
|
|
949
|
+
// Use a command that produces long output
|
|
950
|
+
render(
|
|
951
|
+
<BashRendererWrapper
|
|
952
|
+
initialState={{
|
|
953
|
+
submittedCommand: "python3 -c \"print('x' * 10000)\"",
|
|
954
|
+
runId: 1,
|
|
955
|
+
output: "",
|
|
956
|
+
isRunning: false,
|
|
957
|
+
}}
|
|
958
|
+
onCommandComplete={mockOnCommandComplete}
|
|
959
|
+
commandHistory={[]}
|
|
960
|
+
currentPrompt=""
|
|
961
|
+
availableHeight={20}
|
|
962
|
+
/>,
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
await sleep(1000);
|
|
966
|
+
|
|
967
|
+
expect(mockOnCommandComplete).toHaveBeenCalled();
|
|
968
|
+
const calls = mockOnCommandComplete.mock.calls;
|
|
969
|
+
if (calls.length > 0 && calls[0][0]) {
|
|
970
|
+
expect(calls[0][0].length).toBeGreaterThan(1000);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it("should use normalized command in BashRunner", () => {
|
|
975
|
+
const instance = render(
|
|
976
|
+
<BashRenderer
|
|
977
|
+
state={{
|
|
978
|
+
submittedCommand: " echo hello ",
|
|
979
|
+
runId: 1,
|
|
980
|
+
output: "output",
|
|
981
|
+
isRunning: false,
|
|
982
|
+
}}
|
|
983
|
+
setState={mockSetState}
|
|
984
|
+
onCommandComplete={mockOnCommandComplete}
|
|
985
|
+
commandHistory={[]}
|
|
986
|
+
currentPrompt=""
|
|
987
|
+
availableHeight={20}
|
|
988
|
+
/>,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
logInk(instance);
|
|
992
|
+
|
|
993
|
+
const output = instance.frames.join("");
|
|
994
|
+
// Should show trimmed command
|
|
995
|
+
expect(output).toContain("echo hello");
|
|
996
|
+
expect(output).not.toContain(" echo hello ");
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it("should handle currentPrompt affecting truncation", () => {
|
|
1000
|
+
const commandHistory = [{ command: "cmd1", output: "line1\nline2\nline3\nline4\nline5" }];
|
|
1001
|
+
|
|
1002
|
+
const instance = render(
|
|
1003
|
+
<BashRenderer
|
|
1004
|
+
state={{
|
|
1005
|
+
submittedCommand: null,
|
|
1006
|
+
runId: 0,
|
|
1007
|
+
output: "",
|
|
1008
|
+
isRunning: false,
|
|
1009
|
+
}}
|
|
1010
|
+
setState={mockSetState}
|
|
1011
|
+
onCommandComplete={mockOnCommandComplete}
|
|
1012
|
+
commandHistory={commandHistory}
|
|
1013
|
+
currentPrompt="typing command..."
|
|
1014
|
+
availableHeight={20}
|
|
1015
|
+
/>,
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
logInk(instance);
|
|
1019
|
+
|
|
1020
|
+
const output = instance.frames.join("");
|
|
1021
|
+
expect(output).toBeDefined();
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it("should not call onCommandComplete when command is cancelled by new command", async () => {
|
|
1025
|
+
mockOnCommandComplete.mockClear();
|
|
1026
|
+
|
|
1027
|
+
const firstCommandCallback = vi.fn();
|
|
1028
|
+
const secondCommandCallback = vi.fn();
|
|
1029
|
+
|
|
1030
|
+
const { rerender } = render(
|
|
1031
|
+
<BashRenderer
|
|
1032
|
+
state={{
|
|
1033
|
+
submittedCommand: "sleep 0.3 && echo command1",
|
|
1034
|
+
runId: 1,
|
|
1035
|
+
output: "",
|
|
1036
|
+
isRunning: false,
|
|
1037
|
+
}}
|
|
1038
|
+
setState={mockSetState}
|
|
1039
|
+
onCommandComplete={firstCommandCallback}
|
|
1040
|
+
commandHistory={[]}
|
|
1041
|
+
currentPrompt=""
|
|
1042
|
+
availableHeight={20}
|
|
1043
|
+
/>,
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
// Wait a bit for first command to start
|
|
1047
|
+
await sleep(50);
|
|
1048
|
+
|
|
1049
|
+
// Submit new command before first completes (this should cancel the first)
|
|
1050
|
+
rerender(
|
|
1051
|
+
<BashRenderer
|
|
1052
|
+
state={{
|
|
1053
|
+
submittedCommand: "echo command2",
|
|
1054
|
+
runId: 2,
|
|
1055
|
+
output: "",
|
|
1056
|
+
isRunning: false,
|
|
1057
|
+
}}
|
|
1058
|
+
setState={mockSetState}
|
|
1059
|
+
onCommandComplete={secondCommandCallback}
|
|
1060
|
+
commandHistory={[]}
|
|
1061
|
+
currentPrompt=""
|
|
1062
|
+
availableHeight={20}
|
|
1063
|
+
/>,
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// Wait for both commands to potentially complete
|
|
1067
|
+
await sleep(500);
|
|
1068
|
+
|
|
1069
|
+
// The first command should have been cancelled and not call onCommandComplete
|
|
1070
|
+
expect(firstCommandCallback).not.toHaveBeenCalled();
|
|
1071
|
+
// The second command should have completed and called onCommandComplete
|
|
1072
|
+
expect(secondCommandCallback).toHaveBeenCalled();
|
|
1073
|
+
expect(secondCommandCallback.mock.calls[0][0]).toContain("command2");
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it("should not call onCommandComplete when component unmounts during execution", async () => {
|
|
1077
|
+
mockOnCommandComplete.mockClear();
|
|
1078
|
+
|
|
1079
|
+
const instance = render(
|
|
1080
|
+
<BashRendererWrapper
|
|
1081
|
+
initialState={{
|
|
1082
|
+
submittedCommand: "sleep 0.2 && echo test",
|
|
1083
|
+
runId: 1,
|
|
1084
|
+
output: "",
|
|
1085
|
+
isRunning: false,
|
|
1086
|
+
}}
|
|
1087
|
+
onCommandComplete={mockOnCommandComplete}
|
|
1088
|
+
commandHistory={[]}
|
|
1089
|
+
currentPrompt=""
|
|
1090
|
+
availableHeight={20}
|
|
1091
|
+
/>,
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
logInk(instance);
|
|
1095
|
+
|
|
1096
|
+
// Wait a bit for command to start
|
|
1097
|
+
await sleep(50);
|
|
1098
|
+
|
|
1099
|
+
// Unmount before command completes
|
|
1100
|
+
cleanup();
|
|
1101
|
+
|
|
1102
|
+
// Wait for command to potentially complete
|
|
1103
|
+
await sleep(300);
|
|
1104
|
+
|
|
1105
|
+
// onCommandComplete should not be called for cancelled command
|
|
1106
|
+
expect(mockOnCommandComplete).not.toHaveBeenCalled();
|
|
1107
|
+
});
|
|
1108
|
+
});
|