@lobehub/lobehub 2.0.0-next.50 → 2.0.0-next.52
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/CHANGELOG.md +50 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +25 -5
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +242 -0
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +4 -1
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +499 -0
- package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +357 -0
- package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +30 -22
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +20 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/components.json +6 -0
- package/locales/ar/models.json +119 -126
- package/locales/ar/plugin.json +2 -1
- package/locales/bg-BG/chat.json +20 -0
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/components.json +6 -0
- package/locales/bg-BG/models.json +104 -132
- package/locales/bg-BG/plugin.json +2 -1
- package/locales/de-DE/chat.json +20 -0
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/components.json +6 -0
- package/locales/de-DE/models.json +119 -126
- package/locales/de-DE/plugin.json +2 -1
- package/locales/en-US/chat.json +20 -0
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/components.json +6 -0
- package/locales/en-US/models.json +167 -126
- package/locales/en-US/plugin.json +2 -1
- package/locales/es-ES/chat.json +20 -0
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/components.json +6 -0
- package/locales/es-ES/models.json +119 -126
- package/locales/es-ES/plugin.json +2 -1
- package/locales/fa-IR/chat.json +20 -0
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/components.json +6 -0
- package/locales/fa-IR/models.json +119 -126
- package/locales/fa-IR/plugin.json +2 -1
- package/locales/fr-FR/chat.json +20 -0
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/components.json +6 -0
- package/locales/fr-FR/models.json +119 -126
- package/locales/fr-FR/plugin.json +2 -1
- package/locales/it-IT/chat.json +20 -0
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/components.json +6 -0
- package/locales/it-IT/models.json +119 -126
- package/locales/it-IT/plugin.json +2 -1
- package/locales/ja-JP/chat.json +20 -0
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/components.json +6 -0
- package/locales/ja-JP/models.json +119 -126
- package/locales/ja-JP/plugin.json +2 -1
- package/locales/ko-KR/chat.json +20 -0
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/components.json +6 -0
- package/locales/ko-KR/models.json +119 -126
- package/locales/ko-KR/plugin.json +2 -1
- package/locales/nl-NL/chat.json +20 -0
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/components.json +6 -0
- package/locales/nl-NL/models.json +119 -126
- package/locales/nl-NL/plugin.json +2 -1
- package/locales/pl-PL/chat.json +20 -0
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/components.json +6 -0
- package/locales/pl-PL/models.json +119 -126
- package/locales/pl-PL/plugin.json +2 -1
- package/locales/pt-BR/chat.json +20 -0
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/components.json +6 -0
- package/locales/pt-BR/models.json +119 -126
- package/locales/pt-BR/plugin.json +2 -1
- package/locales/ru-RU/chat.json +20 -0
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/components.json +6 -0
- package/locales/ru-RU/models.json +119 -126
- package/locales/ru-RU/plugin.json +2 -1
- package/locales/tr-TR/chat.json +20 -0
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/components.json +6 -0
- package/locales/tr-TR/models.json +119 -126
- package/locales/tr-TR/plugin.json +2 -1
- package/locales/vi-VN/chat.json +20 -0
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/components.json +6 -0
- package/locales/vi-VN/models.json +119 -126
- package/locales/vi-VN/plugin.json +2 -1
- package/locales/zh-CN/chat.json +20 -0
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/components.json +6 -0
- package/locales/zh-CN/models.json +173 -80
- package/locales/zh-CN/plugin.json +2 -1
- package/locales/zh-TW/chat.json +20 -0
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/components.json +6 -0
- package/locales/zh-TW/models.json +119 -126
- package/locales/zh-TW/plugin.json +2 -1
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/InterventionChecker.ts +1 -1
- package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +23 -23
- package/packages/agent-runtime/src/types/state.ts +7 -1
- package/packages/const/src/settings/tool.ts +1 -5
- package/packages/electron-client-ipc/src/types/localSystem.ts +26 -2
- package/packages/file-loaders/src/loaders/docx/index.ts +1 -1
- package/packages/model-bank/src/aiModels/wenxin.ts +1348 -291
- package/packages/model-runtime/src/core/contextBuilders/openai.test.ts +58 -0
- package/packages/model-runtime/src/core/contextBuilders/openai.ts +24 -10
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +3 -2
- package/packages/model-runtime/src/providers/openai/index.test.ts +44 -0
- package/packages/model-runtime/src/providers/wenxin/index.ts +22 -1
- package/packages/model-runtime/src/utils/modelParse.ts +6 -0
- package/packages/types/src/tool/builtin.ts +15 -4
- package/packages/types/src/tool/intervention.ts +32 -2
- package/packages/types/src/user/settings/tool.ts +3 -27
- package/src/config/modelProviders/wenxin.ts +2 -3
- package/src/features/Conversation/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +133 -0
- package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +48 -0
- package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +2 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/LoadingPlaceholder/index.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/Fallback.tsx +98 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/ModeSelector.tsx +5 -6
- package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/index.tsx +40 -36
- package/src/features/Conversation/Messages/Group/Tool/Render/LoadingPlaceholder/index.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Tool/Render/index.tsx +25 -18
- package/src/features/LocalFile/LocalFile.tsx +55 -5
- package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +10 -4
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +2 -2
- package/src/locales/default/components.ts +6 -0
- package/src/locales/default/plugin.ts +2 -1
- package/src/services/chat/chat.test.ts +1 -0
- package/src/services/electron/localFileService.ts +4 -0
- package/src/store/aiInfra/slices/aiProvider/__tests__/selectors.test.ts +62 -0
- package/src/store/aiInfra/slices/aiProvider/selectors.ts +1 -1
- package/src/store/chat/agents/GeneralChatAgent.ts +26 -1
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +173 -0
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +8 -40
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +91 -34
- package/src/store/user/selectors.ts +1 -0
- package/src/store/user/slices/settings/action.ts +12 -0
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +0 -7
- package/src/store/user/slices/settings/selectors/index.ts +1 -0
- package/src/store/user/slices/settings/selectors/settings.test.ts +0 -37
- package/src/store/user/slices/settings/selectors/settings.ts +0 -5
- package/src/store/user/slices/settings/selectors/toolIntervention.ts +17 -0
- package/src/tools/code-interpreter/Render/index.tsx +1 -1
- package/src/tools/interventions.ts +32 -0
- package/src/tools/local-system/Intervention/RunCommand/index.tsx +56 -0
- package/src/tools/local-system/Placeholder/ListFiles.tsx +3 -5
- package/src/tools/local-system/Placeholder/SearchFiles.tsx +2 -5
- package/src/tools/local-system/Render/ListFiles/index.tsx +16 -21
- package/src/tools/local-system/Render/RenameLocalFile/index.tsx +15 -20
- package/src/tools/local-system/Render/RunCommand/index.tsx +103 -27
- package/src/tools/local-system/Render/SearchFiles/SearchQuery/index.tsx +0 -1
- package/src/tools/local-system/Render/SearchFiles/index.tsx +15 -20
- package/src/tools/local-system/Render/WriteFile/index.tsx +2 -8
- package/src/tools/local-system/index.ts +184 -4
- package/src/tools/local-system/systemRole.ts +62 -8
- package/src/tools/placeholders.ts +39 -8
- package/src/tools/renders.ts +56 -9
- package/src/tools/web-browsing/Placeholder/{PageContent.tsx → CrawlMultiPages.tsx} +4 -1
- package/src/tools/web-browsing/Placeholder/CrawlSinglePage.tsx +12 -0
- package/src/tools/web-browsing/Placeholder/Search.tsx +4 -4
- package/src/tools/web-browsing/Render/CrawlMultiPages.tsx +15 -0
- package/src/tools/web-browsing/Render/CrawlSinglePage.tsx +15 -0
- package/src/tools/web-browsing/Render/Search/index.tsx +39 -44
- package/packages/database/migrations/0044_add_tool_intervention.sql +0 -1
- package/src/tools/local-system/Placeholder/index.tsx +0 -25
- package/src/tools/local-system/Render/index.tsx +0 -40
- package/src/tools/web-browsing/Placeholder/index.tsx +0 -40
- package/src/tools/web-browsing/Render/index.tsx +0 -57
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { App } from '@/core/App';
|
|
4
|
+
|
|
5
|
+
import ShellCommandCtr from '../ShellCommandCtr';
|
|
6
|
+
|
|
7
|
+
// Mock logger
|
|
8
|
+
vi.mock('@/utils/logger', () => ({
|
|
9
|
+
createLogger: () => ({
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock child_process
|
|
18
|
+
vi.mock('node:child_process', () => ({
|
|
19
|
+
spawn: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock crypto
|
|
23
|
+
vi.mock('node:crypto', () => ({
|
|
24
|
+
randomUUID: vi.fn(() => 'test-uuid-123'),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const mockApp = {} as unknown as App;
|
|
28
|
+
|
|
29
|
+
describe('ShellCommandCtr', () => {
|
|
30
|
+
let shellCommandCtr: ShellCommandCtr;
|
|
31
|
+
let mockSpawn: any;
|
|
32
|
+
let mockChildProcess: any;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
|
|
37
|
+
// Import mocks
|
|
38
|
+
const childProcessModule = await import('node:child_process');
|
|
39
|
+
mockSpawn = vi.mocked(childProcessModule.spawn);
|
|
40
|
+
|
|
41
|
+
// Create mock child process
|
|
42
|
+
mockChildProcess = {
|
|
43
|
+
stdout: {
|
|
44
|
+
on: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
stderr: {
|
|
47
|
+
on: vi.fn(),
|
|
48
|
+
},
|
|
49
|
+
on: vi.fn(),
|
|
50
|
+
kill: vi.fn(),
|
|
51
|
+
exitCode: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
mockSpawn.mockReturnValue(mockChildProcess);
|
|
55
|
+
|
|
56
|
+
shellCommandCtr = new ShellCommandCtr(mockApp);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('handleRunCommand', () => {
|
|
60
|
+
describe('synchronous mode', () => {
|
|
61
|
+
it('should execute command successfully', async () => {
|
|
62
|
+
let exitCallback: (code: number) => void;
|
|
63
|
+
let stdoutCallback: (data: Buffer) => void;
|
|
64
|
+
|
|
65
|
+
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
|
66
|
+
if (event === 'exit') {
|
|
67
|
+
exitCallback = callback;
|
|
68
|
+
// Simulate successful exit
|
|
69
|
+
setTimeout(() => exitCallback(0), 10);
|
|
70
|
+
}
|
|
71
|
+
return mockChildProcess;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
|
75
|
+
if (event === 'data') {
|
|
76
|
+
stdoutCallback = callback;
|
|
77
|
+
// Simulate output
|
|
78
|
+
setTimeout(() => stdoutCallback(Buffer.from('test output\n')), 5);
|
|
79
|
+
}
|
|
80
|
+
return mockChildProcess.stdout;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
84
|
+
|
|
85
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
86
|
+
command: 'echo "test"',
|
|
87
|
+
description: 'test command',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.success).toBe(true);
|
|
91
|
+
expect(result.stdout).toBe('test output\n');
|
|
92
|
+
expect(result.exit_code).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle command timeout', async () => {
|
|
96
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
97
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
98
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
99
|
+
|
|
100
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
101
|
+
command: 'sleep 10',
|
|
102
|
+
description: 'long running command',
|
|
103
|
+
timeout: 100,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.success).toBe(false);
|
|
107
|
+
expect(result.error).toContain('timed out');
|
|
108
|
+
expect(mockChildProcess.kill).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle command execution error', async () => {
|
|
112
|
+
let errorCallback: (error: Error) => void;
|
|
113
|
+
|
|
114
|
+
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
|
115
|
+
if (event === 'error') {
|
|
116
|
+
errorCallback = callback;
|
|
117
|
+
setTimeout(() => errorCallback(new Error('Command not found')), 10);
|
|
118
|
+
}
|
|
119
|
+
return mockChildProcess;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
123
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
124
|
+
|
|
125
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
126
|
+
command: 'invalid-command',
|
|
127
|
+
description: 'invalid command',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(false);
|
|
131
|
+
expect(result.error).toBe('Command not found');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle non-zero exit code', async () => {
|
|
135
|
+
let exitCallback: (code: number) => void;
|
|
136
|
+
|
|
137
|
+
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
|
138
|
+
if (event === 'exit') {
|
|
139
|
+
exitCallback = callback;
|
|
140
|
+
setTimeout(() => exitCallback(1), 10);
|
|
141
|
+
}
|
|
142
|
+
return mockChildProcess;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
146
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
147
|
+
|
|
148
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
149
|
+
command: 'exit 1',
|
|
150
|
+
description: 'failing command',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(result.success).toBe(false);
|
|
154
|
+
expect(result.exit_code).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should capture stderr output', async () => {
|
|
158
|
+
let exitCallback: (code: number) => void;
|
|
159
|
+
let stderrCallback: (data: Buffer) => void;
|
|
160
|
+
|
|
161
|
+
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
|
|
162
|
+
if (event === 'exit') {
|
|
163
|
+
exitCallback = callback;
|
|
164
|
+
setTimeout(() => exitCallback(1), 10);
|
|
165
|
+
}
|
|
166
|
+
return mockChildProcess;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
170
|
+
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
|
|
171
|
+
if (event === 'data') {
|
|
172
|
+
stderrCallback = callback;
|
|
173
|
+
setTimeout(() => stderrCallback(Buffer.from('error message\n')), 5);
|
|
174
|
+
}
|
|
175
|
+
return mockChildProcess.stderr;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
179
|
+
command: 'command-with-error',
|
|
180
|
+
description: 'command with stderr',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result.stderr).toBe('error message\n');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should enforce timeout limits', async () => {
|
|
187
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
188
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
189
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
190
|
+
|
|
191
|
+
// Test minimum timeout
|
|
192
|
+
const minResult = await shellCommandCtr.handleRunCommand({
|
|
193
|
+
command: 'sleep 5',
|
|
194
|
+
timeout: 500, // Below 1000ms minimum
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(minResult.success).toBe(false);
|
|
198
|
+
expect(minResult.error).toContain('1000ms'); // Should use 1000ms minimum
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('background mode', () => {
|
|
203
|
+
it('should start command in background', async () => {
|
|
204
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
205
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
206
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
207
|
+
|
|
208
|
+
const result = await shellCommandCtr.handleRunCommand({
|
|
209
|
+
command: 'long-running-task',
|
|
210
|
+
description: 'background task',
|
|
211
|
+
run_in_background: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result.success).toBe(true);
|
|
215
|
+
expect(result.shell_id).toBe('test-uuid-123');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should use correct shell on Windows', async () => {
|
|
219
|
+
const originalPlatform = process.platform;
|
|
220
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
221
|
+
|
|
222
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
223
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
224
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
225
|
+
|
|
226
|
+
await shellCommandCtr.handleRunCommand({
|
|
227
|
+
command: 'dir',
|
|
228
|
+
description: 'windows command',
|
|
229
|
+
run_in_background: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(mockSpawn).toHaveBeenCalledWith('cmd.exe', ['/c', 'dir'], expect.any(Object));
|
|
233
|
+
|
|
234
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should use correct shell on Unix', async () => {
|
|
238
|
+
const originalPlatform = process.platform;
|
|
239
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
240
|
+
|
|
241
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
242
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
243
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
244
|
+
|
|
245
|
+
await shellCommandCtr.handleRunCommand({
|
|
246
|
+
command: 'ls',
|
|
247
|
+
description: 'unix command',
|
|
248
|
+
run_in_background: true,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(mockSpawn).toHaveBeenCalledWith('/bin/sh', ['-c', 'ls'], expect.any(Object));
|
|
252
|
+
|
|
253
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('handleGetCommandOutput', () => {
|
|
259
|
+
beforeEach(async () => {
|
|
260
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
261
|
+
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
|
262
|
+
if (event === 'data') {
|
|
263
|
+
// Simulate some output
|
|
264
|
+
setTimeout(() => callback(Buffer.from('line 1\n')), 5);
|
|
265
|
+
setTimeout(() => callback(Buffer.from('line 2\n')), 10);
|
|
266
|
+
}
|
|
267
|
+
return mockChildProcess.stdout;
|
|
268
|
+
});
|
|
269
|
+
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
|
|
270
|
+
if (event === 'data') {
|
|
271
|
+
setTimeout(() => callback(Buffer.from('error line\n')), 7);
|
|
272
|
+
}
|
|
273
|
+
return mockChildProcess.stderr;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Start a background process first
|
|
277
|
+
await shellCommandCtr.handleRunCommand({
|
|
278
|
+
command: 'test-command',
|
|
279
|
+
run_in_background: true,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should retrieve command output', async () => {
|
|
284
|
+
// Wait for output to be captured
|
|
285
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
286
|
+
|
|
287
|
+
const result = await shellCommandCtr.handleGetCommandOutput({
|
|
288
|
+
shell_id: 'test-uuid-123',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(result.success).toBe(true);
|
|
292
|
+
expect(result.stdout).toContain('line 1');
|
|
293
|
+
expect(result.stderr).toContain('error line');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should return error for non-existent shell_id', async () => {
|
|
297
|
+
const result = await shellCommandCtr.handleGetCommandOutput({
|
|
298
|
+
shell_id: 'non-existent-id',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(result.success).toBe(false);
|
|
302
|
+
expect(result.error).toContain('not found');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should filter output with regex', async () => {
|
|
306
|
+
// Wait for output to be captured
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
308
|
+
|
|
309
|
+
const result = await shellCommandCtr.handleGetCommandOutput({
|
|
310
|
+
shell_id: 'test-uuid-123',
|
|
311
|
+
filter: 'line 1',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.success).toBe(true);
|
|
315
|
+
expect(result.output).toContain('line 1');
|
|
316
|
+
expect(result.output).not.toContain('line 2');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should only return new output since last read', async () => {
|
|
320
|
+
// Wait for initial output
|
|
321
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
322
|
+
|
|
323
|
+
// First read
|
|
324
|
+
const firstResult = await shellCommandCtr.handleGetCommandOutput({
|
|
325
|
+
shell_id: 'test-uuid-123',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(firstResult.stdout).toContain('line 1');
|
|
329
|
+
|
|
330
|
+
// Second read should return empty (no new output)
|
|
331
|
+
const secondResult = await shellCommandCtr.handleGetCommandOutput({
|
|
332
|
+
shell_id: 'test-uuid-123',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(secondResult.stdout).toBe('');
|
|
336
|
+
expect(secondResult.stderr).toBe('');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should handle invalid regex filter gracefully', async () => {
|
|
340
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
341
|
+
|
|
342
|
+
const result = await shellCommandCtr.handleGetCommandOutput({
|
|
343
|
+
shell_id: 'test-uuid-123',
|
|
344
|
+
filter: '[invalid(regex',
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(result.success).toBe(true);
|
|
348
|
+
// Should return unfiltered output when filter is invalid
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should report running status correctly', async () => {
|
|
352
|
+
mockChildProcess.exitCode = null;
|
|
353
|
+
|
|
354
|
+
const runningResult = await shellCommandCtr.handleGetCommandOutput({
|
|
355
|
+
shell_id: 'test-uuid-123',
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(runningResult.running).toBe(true);
|
|
359
|
+
|
|
360
|
+
// Simulate process exit
|
|
361
|
+
mockChildProcess.exitCode = 0;
|
|
362
|
+
|
|
363
|
+
const exitedResult = await shellCommandCtr.handleGetCommandOutput({
|
|
364
|
+
shell_id: 'test-uuid-123',
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(exitedResult.running).toBe(false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should track stdout and stderr offsets separately when streaming output', async () => {
|
|
371
|
+
// Create a new background process with manual control over stdout/stderr
|
|
372
|
+
let stdoutCallback: (data: Buffer) => void;
|
|
373
|
+
let stderrCallback: (data: Buffer) => void;
|
|
374
|
+
|
|
375
|
+
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
|
|
376
|
+
if (event === 'data') {
|
|
377
|
+
stdoutCallback = callback;
|
|
378
|
+
}
|
|
379
|
+
return mockChildProcess.stdout;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
|
|
383
|
+
if (event === 'data') {
|
|
384
|
+
stderrCallback = callback;
|
|
385
|
+
}
|
|
386
|
+
return mockChildProcess.stderr;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Start a new background process
|
|
390
|
+
await shellCommandCtr.handleRunCommand({
|
|
391
|
+
command: 'test-interleaved',
|
|
392
|
+
run_in_background: true,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Simulate stderr output first
|
|
396
|
+
stderrCallback(Buffer.from('error 1\n'));
|
|
397
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
398
|
+
|
|
399
|
+
// First read - should get stderr
|
|
400
|
+
const firstRead = await shellCommandCtr.handleGetCommandOutput({
|
|
401
|
+
shell_id: 'test-uuid-123',
|
|
402
|
+
});
|
|
403
|
+
expect(firstRead.stderr).toBe('error 1\n');
|
|
404
|
+
expect(firstRead.stdout).toBe('');
|
|
405
|
+
|
|
406
|
+
// Simulate stdout output after stderr
|
|
407
|
+
stdoutCallback(Buffer.from('output 1\n'));
|
|
408
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
409
|
+
|
|
410
|
+
// Second read - should get stdout without losing data
|
|
411
|
+
const secondRead = await shellCommandCtr.handleGetCommandOutput({
|
|
412
|
+
shell_id: 'test-uuid-123',
|
|
413
|
+
});
|
|
414
|
+
expect(secondRead.stdout).toBe('output 1\n');
|
|
415
|
+
expect(secondRead.stderr).toBe('');
|
|
416
|
+
|
|
417
|
+
// Simulate more stderr
|
|
418
|
+
stderrCallback(Buffer.from('error 2\n'));
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
420
|
+
|
|
421
|
+
// Third read - should get new stderr
|
|
422
|
+
const thirdRead = await shellCommandCtr.handleGetCommandOutput({
|
|
423
|
+
shell_id: 'test-uuid-123',
|
|
424
|
+
});
|
|
425
|
+
expect(thirdRead.stderr).toBe('error 2\n');
|
|
426
|
+
expect(thirdRead.stdout).toBe('');
|
|
427
|
+
|
|
428
|
+
// Simulate more stdout
|
|
429
|
+
stdoutCallback(Buffer.from('output 2\n'));
|
|
430
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
431
|
+
|
|
432
|
+
// Fourth read - should get new stdout
|
|
433
|
+
const fourthRead = await shellCommandCtr.handleGetCommandOutput({
|
|
434
|
+
shell_id: 'test-uuid-123',
|
|
435
|
+
});
|
|
436
|
+
expect(fourthRead.stdout).toBe('output 2\n');
|
|
437
|
+
expect(fourthRead.stderr).toBe('');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('handleKillCommand', () => {
|
|
442
|
+
beforeEach(async () => {
|
|
443
|
+
mockChildProcess.on.mockImplementation(() => mockChildProcess);
|
|
444
|
+
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
|
|
445
|
+
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
|
|
446
|
+
|
|
447
|
+
// Start a background process
|
|
448
|
+
await shellCommandCtr.handleRunCommand({
|
|
449
|
+
command: 'test-command',
|
|
450
|
+
run_in_background: true,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should kill command successfully', async () => {
|
|
455
|
+
const result = await shellCommandCtr.handleKillCommand({
|
|
456
|
+
shell_id: 'test-uuid-123',
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(result.success).toBe(true);
|
|
460
|
+
expect(mockChildProcess.kill).toHaveBeenCalled();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should return error for non-existent shell_id', async () => {
|
|
464
|
+
const result = await shellCommandCtr.handleKillCommand({
|
|
465
|
+
shell_id: 'non-existent-id',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(result.success).toBe(false);
|
|
469
|
+
expect(result.error).toContain('not found');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should remove process from map after killing', async () => {
|
|
473
|
+
await shellCommandCtr.handleKillCommand({
|
|
474
|
+
shell_id: 'test-uuid-123',
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Try to get output from killed process
|
|
478
|
+
const outputResult = await shellCommandCtr.handleGetCommandOutput({
|
|
479
|
+
shell_id: 'test-uuid-123',
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(outputResult.success).toBe(false);
|
|
483
|
+
expect(outputResult.error).toContain('not found');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should handle kill error gracefully', async () => {
|
|
487
|
+
mockChildProcess.kill.mockImplementation(() => {
|
|
488
|
+
throw new Error('Kill failed');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const result = await shellCommandCtr.handleKillCommand({
|
|
492
|
+
shell_id: 'test-uuid-123',
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(result.success).toBe(false);
|
|
496
|
+
expect(result.error).toBe('Kill failed');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|