@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.
Files changed (171) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +25 -5
  3. package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +242 -0
  4. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +4 -1
  5. package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +499 -0
  6. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +357 -0
  7. package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +30 -22
  8. package/changelog/v1.json +18 -0
  9. package/locales/ar/chat.json +20 -0
  10. package/locales/ar/common.json +1 -0
  11. package/locales/ar/components.json +6 -0
  12. package/locales/ar/models.json +119 -126
  13. package/locales/ar/plugin.json +2 -1
  14. package/locales/bg-BG/chat.json +20 -0
  15. package/locales/bg-BG/common.json +1 -0
  16. package/locales/bg-BG/components.json +6 -0
  17. package/locales/bg-BG/models.json +104 -132
  18. package/locales/bg-BG/plugin.json +2 -1
  19. package/locales/de-DE/chat.json +20 -0
  20. package/locales/de-DE/common.json +1 -0
  21. package/locales/de-DE/components.json +6 -0
  22. package/locales/de-DE/models.json +119 -126
  23. package/locales/de-DE/plugin.json +2 -1
  24. package/locales/en-US/chat.json +20 -0
  25. package/locales/en-US/common.json +1 -0
  26. package/locales/en-US/components.json +6 -0
  27. package/locales/en-US/models.json +167 -126
  28. package/locales/en-US/plugin.json +2 -1
  29. package/locales/es-ES/chat.json +20 -0
  30. package/locales/es-ES/common.json +1 -0
  31. package/locales/es-ES/components.json +6 -0
  32. package/locales/es-ES/models.json +119 -126
  33. package/locales/es-ES/plugin.json +2 -1
  34. package/locales/fa-IR/chat.json +20 -0
  35. package/locales/fa-IR/common.json +1 -0
  36. package/locales/fa-IR/components.json +6 -0
  37. package/locales/fa-IR/models.json +119 -126
  38. package/locales/fa-IR/plugin.json +2 -1
  39. package/locales/fr-FR/chat.json +20 -0
  40. package/locales/fr-FR/common.json +1 -0
  41. package/locales/fr-FR/components.json +6 -0
  42. package/locales/fr-FR/models.json +119 -126
  43. package/locales/fr-FR/plugin.json +2 -1
  44. package/locales/it-IT/chat.json +20 -0
  45. package/locales/it-IT/common.json +1 -0
  46. package/locales/it-IT/components.json +6 -0
  47. package/locales/it-IT/models.json +119 -126
  48. package/locales/it-IT/plugin.json +2 -1
  49. package/locales/ja-JP/chat.json +20 -0
  50. package/locales/ja-JP/common.json +1 -0
  51. package/locales/ja-JP/components.json +6 -0
  52. package/locales/ja-JP/models.json +119 -126
  53. package/locales/ja-JP/plugin.json +2 -1
  54. package/locales/ko-KR/chat.json +20 -0
  55. package/locales/ko-KR/common.json +1 -0
  56. package/locales/ko-KR/components.json +6 -0
  57. package/locales/ko-KR/models.json +119 -126
  58. package/locales/ko-KR/plugin.json +2 -1
  59. package/locales/nl-NL/chat.json +20 -0
  60. package/locales/nl-NL/common.json +1 -0
  61. package/locales/nl-NL/components.json +6 -0
  62. package/locales/nl-NL/models.json +119 -126
  63. package/locales/nl-NL/plugin.json +2 -1
  64. package/locales/pl-PL/chat.json +20 -0
  65. package/locales/pl-PL/common.json +1 -0
  66. package/locales/pl-PL/components.json +6 -0
  67. package/locales/pl-PL/models.json +119 -126
  68. package/locales/pl-PL/plugin.json +2 -1
  69. package/locales/pt-BR/chat.json +20 -0
  70. package/locales/pt-BR/common.json +1 -0
  71. package/locales/pt-BR/components.json +6 -0
  72. package/locales/pt-BR/models.json +119 -126
  73. package/locales/pt-BR/plugin.json +2 -1
  74. package/locales/ru-RU/chat.json +20 -0
  75. package/locales/ru-RU/common.json +1 -0
  76. package/locales/ru-RU/components.json +6 -0
  77. package/locales/ru-RU/models.json +119 -126
  78. package/locales/ru-RU/plugin.json +2 -1
  79. package/locales/tr-TR/chat.json +20 -0
  80. package/locales/tr-TR/common.json +1 -0
  81. package/locales/tr-TR/components.json +6 -0
  82. package/locales/tr-TR/models.json +119 -126
  83. package/locales/tr-TR/plugin.json +2 -1
  84. package/locales/vi-VN/chat.json +20 -0
  85. package/locales/vi-VN/common.json +1 -0
  86. package/locales/vi-VN/components.json +6 -0
  87. package/locales/vi-VN/models.json +119 -126
  88. package/locales/vi-VN/plugin.json +2 -1
  89. package/locales/zh-CN/chat.json +20 -0
  90. package/locales/zh-CN/common.json +1 -0
  91. package/locales/zh-CN/components.json +6 -0
  92. package/locales/zh-CN/models.json +173 -80
  93. package/locales/zh-CN/plugin.json +2 -1
  94. package/locales/zh-TW/chat.json +20 -0
  95. package/locales/zh-TW/common.json +1 -0
  96. package/locales/zh-TW/components.json +6 -0
  97. package/locales/zh-TW/models.json +119 -126
  98. package/locales/zh-TW/plugin.json +2 -1
  99. package/package.json +1 -1
  100. package/packages/agent-runtime/src/core/InterventionChecker.ts +1 -1
  101. package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +23 -23
  102. package/packages/agent-runtime/src/types/state.ts +7 -1
  103. package/packages/const/src/settings/tool.ts +1 -5
  104. package/packages/electron-client-ipc/src/types/localSystem.ts +26 -2
  105. package/packages/file-loaders/src/loaders/docx/index.ts +1 -1
  106. package/packages/model-bank/src/aiModels/wenxin.ts +1348 -291
  107. package/packages/model-runtime/src/core/contextBuilders/openai.test.ts +58 -0
  108. package/packages/model-runtime/src/core/contextBuilders/openai.ts +24 -10
  109. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +3 -2
  110. package/packages/model-runtime/src/providers/openai/index.test.ts +44 -0
  111. package/packages/model-runtime/src/providers/wenxin/index.ts +22 -1
  112. package/packages/model-runtime/src/utils/modelParse.ts +6 -0
  113. package/packages/types/src/tool/builtin.ts +15 -4
  114. package/packages/types/src/tool/intervention.ts +32 -2
  115. package/packages/types/src/user/settings/tool.ts +3 -27
  116. package/src/config/modelProviders/wenxin.ts +2 -3
  117. package/src/features/Conversation/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +133 -0
  118. package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +48 -0
  119. package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +2 -1
  120. package/src/features/Conversation/Messages/Assistant/Tool/Render/LoadingPlaceholder/index.tsx +3 -3
  121. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/Fallback.tsx +98 -0
  122. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/ModeSelector.tsx +5 -6
  123. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/index.tsx +40 -36
  124. package/src/features/Conversation/Messages/Group/Tool/Render/LoadingPlaceholder/index.tsx +3 -3
  125. package/src/features/Conversation/Messages/Group/Tool/Render/index.tsx +25 -18
  126. package/src/features/LocalFile/LocalFile.tsx +55 -5
  127. package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +10 -4
  128. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +2 -2
  129. package/src/locales/default/components.ts +6 -0
  130. package/src/locales/default/plugin.ts +2 -1
  131. package/src/services/chat/chat.test.ts +1 -0
  132. package/src/services/electron/localFileService.ts +4 -0
  133. package/src/store/aiInfra/slices/aiProvider/__tests__/selectors.test.ts +62 -0
  134. package/src/store/aiInfra/slices/aiProvider/selectors.ts +1 -1
  135. package/src/store/chat/agents/GeneralChatAgent.ts +26 -1
  136. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +173 -0
  137. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +8 -40
  138. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +91 -34
  139. package/src/store/user/selectors.ts +1 -0
  140. package/src/store/user/slices/settings/action.ts +12 -0
  141. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +0 -7
  142. package/src/store/user/slices/settings/selectors/index.ts +1 -0
  143. package/src/store/user/slices/settings/selectors/settings.test.ts +0 -37
  144. package/src/store/user/slices/settings/selectors/settings.ts +0 -5
  145. package/src/store/user/slices/settings/selectors/toolIntervention.ts +17 -0
  146. package/src/tools/code-interpreter/Render/index.tsx +1 -1
  147. package/src/tools/interventions.ts +32 -0
  148. package/src/tools/local-system/Intervention/RunCommand/index.tsx +56 -0
  149. package/src/tools/local-system/Placeholder/ListFiles.tsx +3 -5
  150. package/src/tools/local-system/Placeholder/SearchFiles.tsx +2 -5
  151. package/src/tools/local-system/Render/ListFiles/index.tsx +16 -21
  152. package/src/tools/local-system/Render/RenameLocalFile/index.tsx +15 -20
  153. package/src/tools/local-system/Render/RunCommand/index.tsx +103 -27
  154. package/src/tools/local-system/Render/SearchFiles/SearchQuery/index.tsx +0 -1
  155. package/src/tools/local-system/Render/SearchFiles/index.tsx +15 -20
  156. package/src/tools/local-system/Render/WriteFile/index.tsx +2 -8
  157. package/src/tools/local-system/index.ts +184 -4
  158. package/src/tools/local-system/systemRole.ts +62 -8
  159. package/src/tools/placeholders.ts +39 -8
  160. package/src/tools/renders.ts +56 -9
  161. package/src/tools/web-browsing/Placeholder/{PageContent.tsx → CrawlMultiPages.tsx} +4 -1
  162. package/src/tools/web-browsing/Placeholder/CrawlSinglePage.tsx +12 -0
  163. package/src/tools/web-browsing/Placeholder/Search.tsx +4 -4
  164. package/src/tools/web-browsing/Render/CrawlMultiPages.tsx +15 -0
  165. package/src/tools/web-browsing/Render/CrawlSinglePage.tsx +15 -0
  166. package/src/tools/web-browsing/Render/Search/index.tsx +39 -44
  167. package/packages/database/migrations/0044_add_tool_intervention.sql +0 -1
  168. package/src/tools/local-system/Placeholder/index.tsx +0 -25
  169. package/src/tools/local-system/Render/index.tsx +0 -40
  170. package/src/tools/web-browsing/Placeholder/index.tsx +0 -40
  171. 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
+ });