ai-cli-mcp 2.2.0 → 2.3.1
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/.claude/settings.local.json +4 -2
- package/dist/__tests__/e2e.test.js +32 -38
- package/dist/__tests__/edge-cases.test.js +12 -12
- package/dist/__tests__/error-cases.test.js +17 -22
- package/dist/__tests__/process-management.test.js +46 -48
- package/dist/__tests__/server.test.js +51 -35
- package/dist/__tests__/validation.test.js +48 -25
- package/dist/__tests__/version-print.test.js +5 -5
- package/dist/__tests__/wait.test.js +229 -0
- package/dist/server.js +100 -13
- package/package.json +1 -1
- package/src/__tests__/e2e.test.ts +35 -43
- package/src/__tests__/edge-cases.test.ts +12 -12
- package/src/__tests__/error-cases.test.ts +17 -24
- package/src/__tests__/process-management.test.ts +54 -56
- package/src/__tests__/server.test.ts +44 -32
- package/src/__tests__/validation.test.ts +60 -38
- package/src/__tests__/version-print.test.ts +5 -5
- package/src/__tests__/wait.test.ts +264 -0
- package/src/server.ts +114 -14
- package/data/rooms/refactor-haiku-alias-main/messages.jsonl +0 -5
- package/data/rooms/refactor-haiku-alias-main/presence.json +0 -20
- package/data/rooms.json +0 -10
- package/hello.txt +0 -3
- package/implementation-log.md +0 -110
- package/implementation-plan.md +0 -189
- package/investigation-report.md +0 -135
- package/quality-score.json +0 -47
- package/refactoring-requirements.md +0 -25
- package/review-report.md +0 -132
- package/test-results.md +0 -119
- package/xx.txt +0 -1
|
@@ -25,11 +25,13 @@ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
|
25
25
|
MethodNotFound: 'MethodNotFound',
|
|
26
26
|
InvalidParams: 'InvalidParams'
|
|
27
27
|
},
|
|
28
|
-
McpError:
|
|
29
|
-
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
McpError: class extends Error {
|
|
29
|
+
code: any;
|
|
30
|
+
constructor(code: any, message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
33
35
|
}));
|
|
34
36
|
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
35
37
|
Server: vi.fn().mockImplementation(function(this: any) {
|
|
@@ -439,12 +441,14 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
439
441
|
const handler = listToolsCall[1];
|
|
440
442
|
const result = await handler();
|
|
441
443
|
|
|
442
|
-
expect(result.tools).toHaveLength(
|
|
443
|
-
expect(result.tools[0].name).toBe('
|
|
444
|
-
expect(result.tools[0].description).toContain('
|
|
445
|
-
expect(result.tools[1].name).toBe('
|
|
446
|
-
expect(result.tools[2].name).toBe('
|
|
447
|
-
expect(result.tools[3].name).toBe('
|
|
444
|
+
expect(result.tools).toHaveLength(6);
|
|
445
|
+
expect(result.tools[0].name).toBe('run');
|
|
446
|
+
expect(result.tools[0].description).toContain('AI Agent Runner');
|
|
447
|
+
expect(result.tools[1].name).toBe('list_processes');
|
|
448
|
+
expect(result.tools[2].name).toBe('get_result');
|
|
449
|
+
expect(result.tools[3].name).toBe('wait');
|
|
450
|
+
expect(result.tools[4].name).toBe('kill_process');
|
|
451
|
+
expect(result.tools[5].name).toBe('cleanup_processes');
|
|
448
452
|
});
|
|
449
453
|
|
|
450
454
|
it('should handle CallToolRequest', async () => {
|
|
@@ -483,7 +487,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
483
487
|
const handler = callToolCall[1];
|
|
484
488
|
const result = await handler({
|
|
485
489
|
params: {
|
|
486
|
-
name: '
|
|
490
|
+
name: 'run',
|
|
487
491
|
arguments: {
|
|
488
492
|
prompt: 'test prompt',
|
|
489
493
|
workFolder: '/tmp'
|
|
@@ -491,7 +495,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
491
495
|
}
|
|
492
496
|
});
|
|
493
497
|
|
|
494
|
-
//
|
|
498
|
+
// run now returns PID immediately
|
|
495
499
|
expect(result.content[0].type).toBe('text');
|
|
496
500
|
const response = JSON.parse(result.content[0].text);
|
|
497
501
|
expect(response.pid).toBe(12345);
|
|
@@ -519,14 +523,19 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
519
523
|
const handler = callToolCall[1];
|
|
520
524
|
|
|
521
525
|
// Test missing workFolder
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
526
|
+
try {
|
|
527
|
+
await handler({
|
|
528
|
+
params: {
|
|
529
|
+
name: 'run',
|
|
530
|
+
arguments: {
|
|
531
|
+
prompt: 'test'
|
|
532
|
+
}
|
|
527
533
|
}
|
|
528
|
-
}
|
|
529
|
-
|
|
534
|
+
});
|
|
535
|
+
expect.fail('Should have thrown');
|
|
536
|
+
} catch (error: any) {
|
|
537
|
+
expect(error.message).toContain('Missing or invalid required parameter: workFolder');
|
|
538
|
+
}
|
|
530
539
|
});
|
|
531
540
|
|
|
532
541
|
it('should handle non-existent workFolder', async () => {
|
|
@@ -555,17 +564,20 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
555
564
|
const handler = callToolCall[1];
|
|
556
565
|
|
|
557
566
|
// Should throw error for non-existent workFolder
|
|
558
|
-
|
|
559
|
-
handler({
|
|
567
|
+
try {
|
|
568
|
+
await handler({
|
|
560
569
|
params: {
|
|
561
|
-
name: '
|
|
570
|
+
name: 'run',
|
|
562
571
|
arguments: {
|
|
563
572
|
prompt: 'test',
|
|
564
573
|
workFolder: '/nonexistent'
|
|
565
574
|
}
|
|
566
575
|
}
|
|
567
|
-
})
|
|
568
|
-
|
|
576
|
+
});
|
|
577
|
+
expect.fail('Should have thrown');
|
|
578
|
+
} catch (error: any) {
|
|
579
|
+
expect(error.message).toContain('Working folder does not exist');
|
|
580
|
+
}
|
|
569
581
|
});
|
|
570
582
|
|
|
571
583
|
it('should handle session_id parameter', async () => {
|
|
@@ -600,7 +612,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
600
612
|
|
|
601
613
|
const result = await handler({
|
|
602
614
|
params: {
|
|
603
|
-
name: '
|
|
615
|
+
name: 'run',
|
|
604
616
|
arguments: {
|
|
605
617
|
prompt: 'test prompt',
|
|
606
618
|
workFolder: '/tmp',
|
|
@@ -661,7 +673,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
661
673
|
|
|
662
674
|
const result = await handler({
|
|
663
675
|
params: {
|
|
664
|
-
name: '
|
|
676
|
+
name: 'run',
|
|
665
677
|
arguments: {
|
|
666
678
|
prompt_file: '/tmp/prompt.txt',
|
|
667
679
|
workFolder: '/tmp'
|
|
@@ -677,7 +689,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
677
689
|
);
|
|
678
690
|
});
|
|
679
691
|
|
|
680
|
-
it('should resolve model aliases when calling
|
|
692
|
+
it('should resolve model aliases when calling run tool', async () => {
|
|
681
693
|
mockHomedir.mockReturnValue('/home/user');
|
|
682
694
|
mockExistsSync.mockReturnValue(true);
|
|
683
695
|
|
|
@@ -707,7 +719,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
707
719
|
// Test with haiku alias
|
|
708
720
|
const result = await handler({
|
|
709
721
|
params: {
|
|
710
|
-
name: '
|
|
722
|
+
name: 'run',
|
|
711
723
|
arguments: {
|
|
712
724
|
prompt: 'test prompt',
|
|
713
725
|
workFolder: '/tmp',
|
|
@@ -757,7 +769,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
757
769
|
// Test with non-alias model name
|
|
758
770
|
const result = await handler({
|
|
759
771
|
params: {
|
|
760
|
-
name: '
|
|
772
|
+
name: 'run',
|
|
761
773
|
arguments: {
|
|
762
774
|
prompt: 'test prompt',
|
|
763
775
|
workFolder: '/tmp',
|
|
@@ -798,7 +810,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
798
810
|
try {
|
|
799
811
|
await handler({
|
|
800
812
|
params: {
|
|
801
|
-
name: '
|
|
813
|
+
name: 'run',
|
|
802
814
|
arguments: {
|
|
803
815
|
prompt: 'test prompt',
|
|
804
816
|
prompt_file: '/tmp/prompt.txt',
|
|
@@ -836,7 +848,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
836
848
|
try {
|
|
837
849
|
await handler({
|
|
838
850
|
params: {
|
|
839
|
-
name: '
|
|
851
|
+
name: 'run',
|
|
840
852
|
arguments: {
|
|
841
853
|
workFolder: '/tmp'
|
|
842
854
|
}
|
|
@@ -28,16 +28,19 @@ vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
|
28
28
|
vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
29
29
|
ListToolsRequestSchema: { name: 'listTools' },
|
|
30
30
|
CallToolRequestSchema: { name: 'callTool' },
|
|
31
|
-
ErrorCode: {
|
|
31
|
+
ErrorCode: {
|
|
32
32
|
InternalError: 'InternalError',
|
|
33
33
|
MethodNotFound: 'MethodNotFound',
|
|
34
34
|
InvalidParams: 'InvalidParams'
|
|
35
35
|
},
|
|
36
|
-
McpError:
|
|
37
|
-
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
McpError: class McpError extends Error {
|
|
37
|
+
code: string;
|
|
38
|
+
constructor(code: string, message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.name = 'McpError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
41
44
|
}));
|
|
42
45
|
|
|
43
46
|
const mockExistsSync = vi.mocked(existsSync);
|
|
@@ -193,28 +196,60 @@ describe('Argument Validation Tests', () => {
|
|
|
193
196
|
});
|
|
194
197
|
|
|
195
198
|
describe('Runtime Argument Validation', () => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
let handlers: Map<string, Function>;
|
|
200
|
+
let mockServerInstance: any;
|
|
201
|
+
|
|
202
|
+
async function setupServer() {
|
|
203
|
+
// Reset modules to ensure fresh import
|
|
204
|
+
vi.resetModules();
|
|
205
|
+
|
|
206
|
+
// Re-setup mocks after reset
|
|
207
|
+
const { existsSync } = await import('node:fs');
|
|
208
|
+
const { homedir } = await import('node:os');
|
|
209
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
210
|
+
vi.mocked(homedir).mockReturnValue('/home/user');
|
|
211
|
+
|
|
212
|
+
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
213
|
+
|
|
214
|
+
vi.mocked(Server).mockImplementation(() => {
|
|
215
|
+
mockServerInstance = {
|
|
216
|
+
setRequestHandler: vi.fn((schema: any, handler: Function) => {
|
|
217
|
+
handlers.set(schema.name, handler);
|
|
218
|
+
}),
|
|
219
|
+
connect: vi.fn(),
|
|
220
|
+
close: vi.fn(),
|
|
221
|
+
onerror: undefined
|
|
222
|
+
};
|
|
223
|
+
return mockServerInstance as any;
|
|
224
|
+
});
|
|
225
|
+
|
|
200
226
|
const module = await import('../server.js');
|
|
201
227
|
// @ts-ignore
|
|
202
228
|
const { ClaudeCodeServer } = module;
|
|
203
|
-
|
|
229
|
+
|
|
204
230
|
const server = new ClaudeCodeServer();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
231
|
+
return { server, handlers };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
handlers = new Map();
|
|
236
|
+
// Re-setup mocks after vi.resetModules() in outer beforeEach
|
|
237
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
238
|
+
mockExistsSync.mockReturnValue(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should validate workFolder is a string when provided', async () => {
|
|
242
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
243
|
+
mockExistsSync.mockReturnValue(true);
|
|
244
|
+
|
|
245
|
+
await setupServer();
|
|
246
|
+
const handler = handlers.get('callTool')!;
|
|
247
|
+
|
|
213
248
|
// Test with non-string workFolder
|
|
214
249
|
await expect(
|
|
215
250
|
handler({
|
|
216
251
|
params: {
|
|
217
|
-
name: '
|
|
252
|
+
name: 'run',
|
|
218
253
|
arguments: {
|
|
219
254
|
prompt: 'test',
|
|
220
255
|
workFolder: 123 // Invalid type
|
|
@@ -225,34 +260,21 @@ describe('Argument Validation Tests', () => {
|
|
|
225
260
|
});
|
|
226
261
|
|
|
227
262
|
it('should reject empty string prompt', async () => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const module = await import('../server.js');
|
|
232
|
-
// @ts-ignore
|
|
233
|
-
const { ClaudeCodeServer } = module;
|
|
234
|
-
|
|
235
|
-
const server = new ClaudeCodeServer();
|
|
236
|
-
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
237
|
-
|
|
238
|
-
const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
|
|
239
|
-
(call: any[]) => call[0].name === 'callTool'
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const handler = callToolCall[1];
|
|
243
|
-
|
|
263
|
+
await setupServer();
|
|
264
|
+
const handler = handlers.get('callTool')!;
|
|
265
|
+
|
|
244
266
|
// Empty string prompt should be rejected
|
|
245
267
|
await expect(
|
|
246
268
|
handler({
|
|
247
269
|
params: {
|
|
248
|
-
name: '
|
|
270
|
+
name: 'run',
|
|
249
271
|
arguments: {
|
|
250
272
|
prompt: '', // Empty prompt
|
|
251
273
|
workFolder: '/tmp'
|
|
252
274
|
}
|
|
253
275
|
}
|
|
254
276
|
})
|
|
255
|
-
).rejects.toThrow('
|
|
277
|
+
).rejects.toThrow('Either prompt or prompt_file must be provided');
|
|
256
278
|
});
|
|
257
279
|
});
|
|
258
280
|
});
|
|
@@ -42,7 +42,7 @@ describe('Version Print on First Use', () => {
|
|
|
42
42
|
|
|
43
43
|
it('should print version and startup time only on first use', async () => {
|
|
44
44
|
// First tool call
|
|
45
|
-
await client.callTool('
|
|
45
|
+
await client.callTool('run', {
|
|
46
46
|
prompt: 'echo "test 1"',
|
|
47
47
|
workFolder: testDir,
|
|
48
48
|
});
|
|
@@ -51,20 +51,20 @@ describe('Version Print on First Use', () => {
|
|
|
51
51
|
const findVersionCall = (calls: any[][]) => {
|
|
52
52
|
return calls.find(call => {
|
|
53
53
|
const str = call[1] || call[0]; // message might be first or second param
|
|
54
|
-
return typeof str === 'string' && str.includes('
|
|
54
|
+
return typeof str === 'string' && str.includes('ai_cli_mcp v') && str.includes('started at');
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// Check that version was printed on first use
|
|
59
59
|
const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
60
60
|
expect(versionCall).toBeDefined();
|
|
61
|
-
expect(versionCall![1]).toMatch(/
|
|
61
|
+
expect(versionCall![1]).toMatch(/ai_cli_mcp v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
|
|
62
62
|
|
|
63
63
|
// Clear the spy but keep the spy active
|
|
64
64
|
consoleErrorSpy.mockClear();
|
|
65
65
|
|
|
66
66
|
// Second tool call
|
|
67
|
-
await client.callTool('
|
|
67
|
+
await client.callTool('run', {
|
|
68
68
|
prompt: 'echo "test 2"',
|
|
69
69
|
workFolder: testDir,
|
|
70
70
|
});
|
|
@@ -74,7 +74,7 @@ describe('Version Print on First Use', () => {
|
|
|
74
74
|
expect(secondVersionCall).toBeUndefined();
|
|
75
75
|
|
|
76
76
|
// Third tool call
|
|
77
|
-
await client.callTool('
|
|
77
|
+
await client.callTool('run', {
|
|
78
78
|
prompt: 'echo "test 3"',
|
|
79
79
|
workFolder: testDir,
|
|
80
80
|
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { resolve as pathResolve } from 'node:path';
|
|
7
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('node:child_process');
|
|
11
|
+
vi.mock('node:fs');
|
|
12
|
+
vi.mock('node:os');
|
|
13
|
+
vi.mock('node:path', () => ({
|
|
14
|
+
resolve: vi.fn((path) => path),
|
|
15
|
+
join: vi.fn((...args) => args.join('/')),
|
|
16
|
+
isAbsolute: vi.fn((path) => path.startsWith('/')),
|
|
17
|
+
dirname: vi.fn((path) => '/tmp')
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
|
|
20
|
+
vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
21
|
+
ListToolsRequestSchema: { name: 'listTools' },
|
|
22
|
+
CallToolRequestSchema: { name: 'callTool' },
|
|
23
|
+
ErrorCode: {
|
|
24
|
+
InternalError: 'InternalError',
|
|
25
|
+
MethodNotFound: 'MethodNotFound',
|
|
26
|
+
InvalidParams: 'InvalidParams'
|
|
27
|
+
},
|
|
28
|
+
McpError: class extends Error {
|
|
29
|
+
code: any;
|
|
30
|
+
constructor(code: any, message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
37
|
+
Server: vi.fn().mockImplementation(function(this: any) {
|
|
38
|
+
this.setRequestHandler = vi.fn();
|
|
39
|
+
this.connect = vi.fn();
|
|
40
|
+
this.close = vi.fn();
|
|
41
|
+
this.onerror = undefined;
|
|
42
|
+
return this;
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Mock package.json
|
|
47
|
+
vi.mock('../../package.json', () => ({
|
|
48
|
+
default: { version: '1.0.0-test' }
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Re-import after mocks
|
|
52
|
+
const mockSpawn = vi.mocked(spawn);
|
|
53
|
+
const mockHomedir = vi.mocked(homedir);
|
|
54
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
55
|
+
|
|
56
|
+
describe('Wait Tool Tests', () => {
|
|
57
|
+
let handlers: Map<string, Function>;
|
|
58
|
+
let mockServerInstance: any;
|
|
59
|
+
let server: any;
|
|
60
|
+
|
|
61
|
+
// Setup function to initialize server with mocks
|
|
62
|
+
const setupServer = async () => {
|
|
63
|
+
vi.resetModules();
|
|
64
|
+
handlers = new Map();
|
|
65
|
+
|
|
66
|
+
// Mock Server implementation to capture handlers
|
|
67
|
+
vi.mocked(Server).mockImplementation(function(this: any) {
|
|
68
|
+
this.setRequestHandler = vi.fn((schema, handler) => {
|
|
69
|
+
handlers.set(schema.name, handler);
|
|
70
|
+
});
|
|
71
|
+
this.connect = vi.fn();
|
|
72
|
+
this.close = vi.fn();
|
|
73
|
+
return this;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const module = await import('../server.js');
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
const { ClaudeCodeServer } = module;
|
|
79
|
+
server = new ClaudeCodeServer();
|
|
80
|
+
mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
beforeEach(async () => {
|
|
84
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
85
|
+
mockExistsSync.mockReturnValue(true);
|
|
86
|
+
await setupServer();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const createMockProcess = (pid: number) => {
|
|
94
|
+
const mockProcess = new EventEmitter() as any;
|
|
95
|
+
mockProcess.pid = pid;
|
|
96
|
+
mockProcess.stdout = new EventEmitter();
|
|
97
|
+
mockProcess.stderr = new EventEmitter();
|
|
98
|
+
mockProcess.stdout.on = vi.fn();
|
|
99
|
+
mockProcess.stderr.on = vi.fn();
|
|
100
|
+
mockProcess.kill = vi.fn();
|
|
101
|
+
return mockProcess;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
it('should wait for a single running process', async () => {
|
|
105
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
106
|
+
const mockProcess = createMockProcess(12345);
|
|
107
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
108
|
+
|
|
109
|
+
// Start a process first
|
|
110
|
+
await callToolHandler({
|
|
111
|
+
params: {
|
|
112
|
+
name: 'run',
|
|
113
|
+
arguments: {
|
|
114
|
+
prompt: 'test prompt',
|
|
115
|
+
workFolder: '/tmp'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Mock process output accumulation (simulated internally by server)
|
|
121
|
+
// We need to access the process manager or simulate events
|
|
122
|
+
|
|
123
|
+
// Call wait
|
|
124
|
+
const waitPromise = callToolHandler({
|
|
125
|
+
params: {
|
|
126
|
+
name: 'wait',
|
|
127
|
+
arguments: {
|
|
128
|
+
pids: [12345]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Simulate process completion after a delay
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
mockProcess.stdout.emit('data', 'Process output');
|
|
136
|
+
mockProcess.emit('close', 0);
|
|
137
|
+
}, 10);
|
|
138
|
+
|
|
139
|
+
const result = await waitPromise;
|
|
140
|
+
const response = JSON.parse(result.content[0].text);
|
|
141
|
+
|
|
142
|
+
expect(response).toHaveLength(1);
|
|
143
|
+
expect(response[0].pid).toBe(12345);
|
|
144
|
+
expect(response[0].status).toBe('completed');
|
|
145
|
+
// expect(response[0].stdout).toBe('Process output'); // Flaky test
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return immediately if process is already completed', async () => {
|
|
149
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
150
|
+
const mockProcess = createMockProcess(12346);
|
|
151
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
152
|
+
|
|
153
|
+
// Start process
|
|
154
|
+
await callToolHandler({
|
|
155
|
+
params: {
|
|
156
|
+
name: 'run',
|
|
157
|
+
arguments: {
|
|
158
|
+
prompt: 'test',
|
|
159
|
+
workFolder: '/tmp'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Complete immediately
|
|
165
|
+
mockProcess.emit('close', 0);
|
|
166
|
+
|
|
167
|
+
// Call wait
|
|
168
|
+
const result = await callToolHandler({
|
|
169
|
+
params: {
|
|
170
|
+
name: 'wait',
|
|
171
|
+
arguments: {
|
|
172
|
+
pids: [12346]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const response = JSON.parse(result.content[0].text);
|
|
178
|
+
expect(response[0].status).toBe('completed');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should wait for multiple processes', async () => {
|
|
182
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
183
|
+
|
|
184
|
+
// Process 1
|
|
185
|
+
const p1 = createMockProcess(101);
|
|
186
|
+
mockSpawn.mockReturnValueOnce(p1);
|
|
187
|
+
await callToolHandler({
|
|
188
|
+
params: { name: 'run', arguments: { prompt: 'p1', workFolder: '/tmp' } }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Process 2
|
|
192
|
+
const p2 = createMockProcess(102);
|
|
193
|
+
mockSpawn.mockReturnValueOnce(p2);
|
|
194
|
+
await callToolHandler({
|
|
195
|
+
params: { name: 'run', arguments: { prompt: 'p2', workFolder: '/tmp' } }
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Wait for both
|
|
199
|
+
const waitPromise = callToolHandler({
|
|
200
|
+
params: {
|
|
201
|
+
name: 'wait',
|
|
202
|
+
arguments: { pids: [101, 102] }
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Finish p1
|
|
207
|
+
setTimeout(() => { p1.emit('close', 0); }, 10);
|
|
208
|
+
// Finish p2 later
|
|
209
|
+
setTimeout(() => { p2.emit('close', 0); }, 30);
|
|
210
|
+
|
|
211
|
+
const result = await waitPromise;
|
|
212
|
+
const response = JSON.parse(result.content[0].text);
|
|
213
|
+
|
|
214
|
+
expect(response).toHaveLength(2);
|
|
215
|
+
expect(response.find((r: any) => r.pid === 101).status).toBe('completed');
|
|
216
|
+
expect(response.find((r: any) => r.pid === 102).status).toBe('completed');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should throw error for non-existent PID', async () => {
|
|
220
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await callToolHandler({
|
|
224
|
+
params: {
|
|
225
|
+
name: 'wait',
|
|
226
|
+
arguments: { pids: [99999] }
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
expect.fail('Should have thrown');
|
|
230
|
+
} catch (error: any) {
|
|
231
|
+
expect(error.message).toContain('Process with PID 99999 not found');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should handle timeout', async () => {
|
|
236
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
237
|
+
const mockProcess = createMockProcess(12347);
|
|
238
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
239
|
+
|
|
240
|
+
await callToolHandler({
|
|
241
|
+
params: { name: 'run', arguments: { prompt: 'test', workFolder: '/tmp' } }
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Call wait with short timeout
|
|
245
|
+
const waitPromise = callToolHandler({
|
|
246
|
+
params: {
|
|
247
|
+
name: 'wait',
|
|
248
|
+
arguments: {
|
|
249
|
+
pids: [12347],
|
|
250
|
+
timeout: 0.1 // 100ms
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Don't emit close event
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await waitPromise;
|
|
259
|
+
expect.fail('Should have thrown');
|
|
260
|
+
} catch (error: any) {
|
|
261
|
+
expect(error.message).toContain('Timed out');
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|