@lobehub/lobehub 2.0.0-next.50 → 2.0.0-next.51
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 +25 -0
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +242 -0
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +499 -0
- package/changelog/v1.json +9 -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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/plugin.json +1 -0
- 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/file-loaders/src/loaders/docx/index.ts +1 -1
- package/packages/model-bank/src/aiModels/wenxin.ts +1348 -291
- 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 +9 -0
- 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/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/index.tsx +25 -18
- package/src/features/LocalFile/LocalFile.tsx +55 -5
- package/src/locales/default/components.ts +6 -0
- package/src/locales/default/plugin.ts +1 -0
- package/src/services/electron/localFileService.ts +4 -0
- 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/interventions.ts +8 -0
- package/src/tools/local-system/Intervention/RunCommand/index.tsx +56 -0
- package/src/tools/local-system/Intervention/index.tsx +17 -0
- package/src/tools/local-system/Render/RunCommand/index.tsx +100 -21
- package/src/tools/local-system/Render/index.tsx +2 -0
- package/src/tools/local-system/index.ts +180 -0
- package/src/tools/local-system/systemRole.ts +61 -7
|
@@ -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
|
+
});
|
package/changelog/v1.json
CHANGED
package/locales/ar/chat.json
CHANGED
|
@@ -369,6 +369,26 @@
|
|
|
369
369
|
"remained": "متبقي",
|
|
370
370
|
"used": "مستخدم"
|
|
371
371
|
},
|
|
372
|
+
"tool": {
|
|
373
|
+
"intervention": {
|
|
374
|
+
"approve": "الموافقة",
|
|
375
|
+
"approveAndRemember": "الموافقة والتذكر",
|
|
376
|
+
"approveOnce": "الموافقة لمرة واحدة فقط",
|
|
377
|
+
"mode": {
|
|
378
|
+
"allowList": "قائمة السماح",
|
|
379
|
+
"allowListDesc": "تنفيذ الأدوات المعتمدة فقط تلقائيًا",
|
|
380
|
+
"autoRun": "الموافقة التلقائية",
|
|
381
|
+
"autoRunDesc": "الموافقة تلقائيًا على تنفيذ جميع الأدوات",
|
|
382
|
+
"manual": "يدوي",
|
|
383
|
+
"manualDesc": "يتطلب الموافقة اليدوية في كل مرة يتم فيها الاستدعاء"
|
|
384
|
+
},
|
|
385
|
+
"reject": "رفض",
|
|
386
|
+
"rejectReasonPlaceholder": "إدخال سبب الرفض سيساعد الوكيل على الفهم وتحسين الإجراءات المستقبلية",
|
|
387
|
+
"rejectTitle": "رفض استدعاء الأداة هذه المرة",
|
|
388
|
+
"rejectedWithReason": "تم رفض استدعاء الأداة هذه المرة بشكل يدوي: {{reason}}",
|
|
389
|
+
"toolRejected": "تم رفض استدعاء الأداة هذه المرة بشكل يدوي"
|
|
390
|
+
}
|
|
391
|
+
},
|
|
372
392
|
"topic": {
|
|
373
393
|
"checkOpenNewTopic": "هل ترغب في فتح موضوع جديد؟",
|
|
374
394
|
"checkSaveCurrentMessages": "هل ترغب في حفظ الدردشة الحالية كموضوع؟",
|
package/locales/ar/common.json
CHANGED
package/locales/ar/plugin.json
CHANGED
package/locales/bg-BG/chat.json
CHANGED
|
@@ -369,6 +369,26 @@
|
|
|
369
369
|
"remained": "Оставащи",
|
|
370
370
|
"used": "Използвани"
|
|
371
371
|
},
|
|
372
|
+
"tool": {
|
|
373
|
+
"intervention": {
|
|
374
|
+
"approve": "Одобряване",
|
|
375
|
+
"approveAndRemember": "Одобряване и запомняне",
|
|
376
|
+
"approveOnce": "Одобряване само този път",
|
|
377
|
+
"mode": {
|
|
378
|
+
"allowList": "Бял списък",
|
|
379
|
+
"allowListDesc": "Автоматично се изпълняват само одобрените инструменти",
|
|
380
|
+
"autoRun": "Автоматично одобрение",
|
|
381
|
+
"autoRunDesc": "Автоматично одобряване на всички изпълнения на инструменти",
|
|
382
|
+
"manual": "Ръчно",
|
|
383
|
+
"manualDesc": "Необходимо е ръчно одобрение при всяко извикване"
|
|
384
|
+
},
|
|
385
|
+
"reject": "Отхвърляне",
|
|
386
|
+
"rejectReasonPlaceholder": "Въведете причина за отхвърляне, за да помогнете на агента да разбере и подобри бъдещите действия",
|
|
387
|
+
"rejectTitle": "Отхвърляне на това извикване на инструмент",
|
|
388
|
+
"rejectedWithReason": "Това извикване на инструмент беше умишлено отхвърлено: {{reason}}",
|
|
389
|
+
"toolRejected": "Това извикване на инструмент беше умишлено отхвърлено"
|
|
390
|
+
}
|
|
391
|
+
},
|
|
372
392
|
"topic": {
|
|
373
393
|
"checkOpenNewTopic": "Да се отвори ли нова тема?",
|
|
374
394
|
"checkSaveCurrentMessages": "Искате ли да запазите текущата сесия като тема?",
|
|
@@ -106,6 +106,12 @@
|
|
|
106
106
|
"keyPlaceholder": "Ключ",
|
|
107
107
|
"valuePlaceholder": "Стойност"
|
|
108
108
|
},
|
|
109
|
+
"LocalFile": {
|
|
110
|
+
"action": {
|
|
111
|
+
"open": "Отвори",
|
|
112
|
+
"showInFolder": "Показване в папката"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
109
115
|
"MaxTokenSlider": {
|
|
110
116
|
"unlimited": "Неограничено"
|
|
111
117
|
},
|
|
@@ -255,6 +255,7 @@
|
|
|
255
255
|
"moveLocalFiles": "Преместване на файлове",
|
|
256
256
|
"readLocalFile": "Четене на съдържание на файл",
|
|
257
257
|
"renameLocalFile": "Преименуване",
|
|
258
|
+
"runCommand": "Изпълни код",
|
|
258
259
|
"searchLocalFiles": "Търсене на файлове",
|
|
259
260
|
"writeLocalFile": "Запис в файл"
|
|
260
261
|
},
|
package/locales/de-DE/chat.json
CHANGED
|
@@ -369,6 +369,26 @@
|
|
|
369
369
|
"remained": "Verbleibend",
|
|
370
370
|
"used": "Verwendet"
|
|
371
371
|
},
|
|
372
|
+
"tool": {
|
|
373
|
+
"intervention": {
|
|
374
|
+
"approve": "Genehmigen",
|
|
375
|
+
"approveAndRemember": "Genehmigen und merken",
|
|
376
|
+
"approveOnce": "Nur dieses Mal genehmigen",
|
|
377
|
+
"mode": {
|
|
378
|
+
"allowList": "Positivliste",
|
|
379
|
+
"allowListDesc": "Nur automatisch genehmigte Tools ausführen",
|
|
380
|
+
"autoRun": "Automatisch genehmigen",
|
|
381
|
+
"autoRunDesc": "Alle Tool-Ausführungen automatisch genehmigen",
|
|
382
|
+
"manual": "Manuell",
|
|
383
|
+
"manualDesc": "Jede Ausführung muss manuell genehmigt werden"
|
|
384
|
+
},
|
|
385
|
+
"reject": "Ablehnen",
|
|
386
|
+
"rejectReasonPlaceholder": "Die Angabe eines Ablehnungsgrundes hilft dem Agenten, zukünftige Aktionen zu verbessern",
|
|
387
|
+
"rejectTitle": "Tool-Ausführung ablehnen",
|
|
388
|
+
"rejectedWithReason": "Die Tool-Ausführung wurde abgelehnt: {{reason}}",
|
|
389
|
+
"toolRejected": "Die Tool-Ausführung wurde abgelehnt"
|
|
390
|
+
}
|
|
391
|
+
},
|
|
372
392
|
"topic": {
|
|
373
393
|
"checkOpenNewTopic": "Soll ein neues Thema eröffnet werden?",
|
|
374
394
|
"checkSaveCurrentMessages": "Möchten Sie die aktuelle Konversation als Thema speichern?",
|
|
@@ -106,6 +106,12 @@
|
|
|
106
106
|
"keyPlaceholder": "Schlüssel",
|
|
107
107
|
"valuePlaceholder": "Wert"
|
|
108
108
|
},
|
|
109
|
+
"LocalFile": {
|
|
110
|
+
"action": {
|
|
111
|
+
"open": "Öffnen",
|
|
112
|
+
"showInFolder": "Im Ordner anzeigen"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
109
115
|
"MaxTokenSlider": {
|
|
110
116
|
"unlimited": "Unbegrenzt"
|
|
111
117
|
},
|