ai-cli-mcp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/test.yml +43 -0
  4. package/.vscode/settings.json +3 -0
  5. package/AGENT.md +57 -0
  6. package/CHANGELOG.md +126 -0
  7. package/LICENSE +22 -0
  8. package/README.md +329 -0
  9. package/RELEASE.md +74 -0
  10. package/data/rooms/refactor-haiku-alias-main/messages.jsonl +5 -0
  11. package/data/rooms/refactor-haiku-alias-main/presence.json +20 -0
  12. package/data/rooms.json +10 -0
  13. package/dist/__tests__/e2e.test.js +238 -0
  14. package/dist/__tests__/edge-cases.test.js +135 -0
  15. package/dist/__tests__/error-cases.test.js +296 -0
  16. package/dist/__tests__/mocks.js +32 -0
  17. package/dist/__tests__/model-alias.test.js +36 -0
  18. package/dist/__tests__/process-management.test.js +632 -0
  19. package/dist/__tests__/server.test.js +665 -0
  20. package/dist/__tests__/setup.js +11 -0
  21. package/dist/__tests__/utils/claude-mock.js +80 -0
  22. package/dist/__tests__/utils/mcp-client.js +104 -0
  23. package/dist/__tests__/utils/persistent-mock.js +25 -0
  24. package/dist/__tests__/utils/test-helpers.js +11 -0
  25. package/dist/__tests__/validation.test.js +212 -0
  26. package/dist/__tests__/version-print.test.js +69 -0
  27. package/dist/parsers.js +54 -0
  28. package/dist/server.js +614 -0
  29. package/docs/RELEASE_CHECKLIST.md +26 -0
  30. package/docs/e2e-testing.md +148 -0
  31. package/docs/local_install.md +111 -0
  32. package/hello.txt +3 -0
  33. package/implementation-log.md +110 -0
  34. package/implementation-plan.md +189 -0
  35. package/investigation-report.md +135 -0
  36. package/package.json +53 -0
  37. package/print-eslint-config.js +3 -0
  38. package/quality-score.json +47 -0
  39. package/refactoring-requirements.md +25 -0
  40. package/review-report.md +132 -0
  41. package/scripts/check-version-log.sh +34 -0
  42. package/scripts/publish-release.sh +95 -0
  43. package/scripts/restore-config.sh +28 -0
  44. package/scripts/test-release.sh +69 -0
  45. package/src/__tests__/e2e.test.ts +290 -0
  46. package/src/__tests__/edge-cases.test.ts +181 -0
  47. package/src/__tests__/error-cases.test.ts +378 -0
  48. package/src/__tests__/mocks.ts +35 -0
  49. package/src/__tests__/model-alias.test.ts +44 -0
  50. package/src/__tests__/process-management.test.ts +772 -0
  51. package/src/__tests__/server.test.ts +851 -0
  52. package/src/__tests__/setup.ts +13 -0
  53. package/src/__tests__/utils/claude-mock.ts +87 -0
  54. package/src/__tests__/utils/mcp-client.ts +129 -0
  55. package/src/__tests__/utils/persistent-mock.ts +29 -0
  56. package/src/__tests__/utils/test-helpers.ts +13 -0
  57. package/src/__tests__/validation.test.ts +258 -0
  58. package/src/__tests__/version-print.test.ts +86 -0
  59. package/src/parsers.ts +55 -0
  60. package/src/server.ts +735 -0
  61. package/start.bat +9 -0
  62. package/start.sh +21 -0
  63. package/test-results.md +119 -0
  64. package/test-standalone.js +5877 -0
  65. package/tsconfig.json +16 -0
  66. package/vitest.config.e2e.ts +27 -0
  67. package/vitest.config.ts +22 -0
  68. package/vitest.config.unit.ts +29 -0
  69. package/xx.txt +1 -0
@@ -0,0 +1,772 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { EventEmitter } from 'node:events';
6
+
7
+ // Mock dependencies
8
+ vi.mock('node:child_process');
9
+ vi.mock('node:fs');
10
+ vi.mock('node:os');
11
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
12
+ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
13
+ ListToolsRequestSchema: { name: 'listTools' },
14
+ CallToolRequestSchema: { name: 'callTool' },
15
+ ErrorCode: {
16
+ InternalError: 'InternalError',
17
+ MethodNotFound: 'MethodNotFound',
18
+ InvalidParams: 'InvalidParams'
19
+ },
20
+ McpError: class McpError extends Error {
21
+ code: string;
22
+ constructor(code: string, message: string) {
23
+ super(message);
24
+ this.code = code;
25
+ this.name = 'McpError';
26
+ }
27
+ }
28
+ }));
29
+ vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
30
+ Server: vi.fn().mockImplementation(function() {
31
+ return {
32
+ setRequestHandler: vi.fn(),
33
+ connect: vi.fn(),
34
+ close: vi.fn(),
35
+ onerror: undefined,
36
+ };
37
+ }),
38
+ }));
39
+
40
+ const mockExistsSync = vi.mocked(existsSync);
41
+ const mockSpawn = vi.mocked(spawn);
42
+ const mockHomedir = vi.mocked(homedir);
43
+
44
+ describe('Process Management Tests', () => {
45
+ let consoleErrorSpy: any;
46
+ let originalEnv: any;
47
+ let mockServerInstance: any;
48
+ let handlers: Map<string, Function>;
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ vi.resetModules();
53
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
54
+ originalEnv = { ...process.env };
55
+ process.env = { ...originalEnv };
56
+ handlers = new Map();
57
+
58
+ // Set up default mocks
59
+ mockHomedir.mockReturnValue('/home/user');
60
+ mockExistsSync.mockReturnValue(true);
61
+ });
62
+
63
+ afterEach(() => {
64
+ consoleErrorSpy.mockRestore();
65
+ process.env = originalEnv;
66
+ });
67
+
68
+ async function setupServer() {
69
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
70
+
71
+ vi.mocked(Server).mockImplementation(() => {
72
+ mockServerInstance = {
73
+ setRequestHandler: vi.fn((schema: any, handler: Function) => {
74
+ handlers.set(schema.name, handler);
75
+ }),
76
+ connect: vi.fn(),
77
+ close: vi.fn(),
78
+ onerror: undefined
79
+ };
80
+ return mockServerInstance as any;
81
+ });
82
+
83
+ const module = await import('../server.js');
84
+ const { ClaudeCodeServer } = module;
85
+
86
+ const server = new ClaudeCodeServer();
87
+
88
+ return { server, module, handlers };
89
+ }
90
+
91
+ describe('claude_code tool with PID return', () => {
92
+ it('should return PID immediately when starting a process', async () => {
93
+ const { handlers } = await setupServer();
94
+
95
+ // Create a mock process
96
+ const mockProcess = new EventEmitter() as any;
97
+ mockProcess.pid = 12345;
98
+ mockProcess.stdout = new EventEmitter();
99
+ mockProcess.stderr = new EventEmitter();
100
+ mockProcess.kill = vi.fn();
101
+
102
+ mockSpawn.mockReturnValue(mockProcess);
103
+
104
+ const callToolHandler = handlers.get('callTool')!;
105
+ const result = await callToolHandler!({
106
+ params: {
107
+ name: 'claude_code',
108
+ arguments: {
109
+ prompt: 'test prompt',
110
+ workFolder: '/tmp'
111
+ }
112
+ }
113
+ });
114
+
115
+ const response = JSON.parse(result.content[0].text);
116
+ expect(response.pid).toBe(12345);
117
+ expect(response.status).toBe('started');
118
+ expect(response.message).toBe('Claude Code process started successfully');
119
+ });
120
+
121
+ it('should handle process with model parameter', async () => {
122
+ const { handlers } = await setupServer();
123
+
124
+ const mockProcess = new EventEmitter() as any;
125
+ mockProcess.pid = 12346;
126
+ mockProcess.stdout = new EventEmitter();
127
+ mockProcess.stderr = new EventEmitter();
128
+ mockProcess.kill = vi.fn();
129
+
130
+ mockSpawn.mockReturnValue(mockProcess);
131
+
132
+ const callToolHandler = handlers.get('callTool')!;
133
+ await callToolHandler!({
134
+ params: {
135
+ name: 'claude_code',
136
+ arguments: {
137
+ prompt: 'test prompt',
138
+ workFolder: '/tmp',
139
+ model: 'opus'
140
+ }
141
+ }
142
+ });
143
+
144
+ expect(mockSpawn).toHaveBeenCalledWith(
145
+ expect.any(String),
146
+ expect.arrayContaining(['--model', 'opus']),
147
+ expect.any(Object)
148
+ );
149
+ });
150
+
151
+ it('should handle Japanese prompts with newlines', async () => {
152
+ const { handlers } = await setupServer();
153
+
154
+ const mockProcess = new EventEmitter() as any;
155
+ mockProcess.pid = 12360;
156
+ mockProcess.stdout = new EventEmitter();
157
+ mockProcess.stderr = new EventEmitter();
158
+ mockProcess.kill = vi.fn();
159
+
160
+ mockSpawn.mockReturnValue(mockProcess);
161
+
162
+ const japanesePrompt = `日本語のテストプロンプトです。
163
+ これは改行を含んでいます。
164
+ さらに、特殊文字も含みます:「こんにちは」、『世界』
165
+ 最後の行です。`;
166
+
167
+ const callToolHandler = handlers.get('callTool')!;
168
+ const result = await callToolHandler!({
169
+ params: {
170
+ name: 'claude_code',
171
+ arguments: {
172
+ prompt: japanesePrompt,
173
+ workFolder: '/tmp'
174
+ }
175
+ }
176
+ });
177
+
178
+ // Verify PID is returned
179
+ const response = JSON.parse(result.content[0].text);
180
+ expect(response.pid).toBe(12360);
181
+
182
+ // Verify spawn was called with the correct prompt including newlines
183
+ expect(mockSpawn).toHaveBeenCalledWith(
184
+ expect.any(String),
185
+ expect.arrayContaining(['-p', japanesePrompt]),
186
+ expect.any(Object)
187
+ );
188
+
189
+ // Verify the prompt is stored correctly in process manager
190
+ const getResult = await callToolHandler!({
191
+ params: {
192
+ name: 'get_claude_result',
193
+ arguments: {
194
+ pid: 12360
195
+ }
196
+ }
197
+ });
198
+
199
+ const processInfo = JSON.parse(getResult.content[0].text);
200
+ expect(processInfo.prompt).toBe(japanesePrompt);
201
+ });
202
+
203
+ it('should handle very long Japanese prompts with multiple paragraphs', async () => {
204
+ const { handlers } = await setupServer();
205
+
206
+ const mockProcess = new EventEmitter() as any;
207
+ mockProcess.pid = 12361;
208
+ mockProcess.stdout = new EventEmitter();
209
+ mockProcess.stderr = new EventEmitter();
210
+ mockProcess.kill = vi.fn();
211
+
212
+ mockSpawn.mockReturnValue(mockProcess);
213
+
214
+ const longJapanesePrompt = `# タスク:ファイル管理システムの作成
215
+
216
+ 以下の要件に従って、ファイル管理システムを作成してください:
217
+
218
+ 1. **基本機能**
219
+ - ファイルの作成、読み取り、更新、削除(CRUD)
220
+ - ディレクトリの作成と管理
221
+ - ファイルの検索機能
222
+
223
+ 2. **追加機能**
224
+ - ファイルのバージョン管理
225
+ - アクセス権限の設定
226
+ - ログ記録機能
227
+
228
+ 3. **技術要件**
229
+ - TypeScriptを使用
230
+ - テストコードを含める
231
+ - ドキュメントを日本語で作成
232
+
233
+ 注意事項:
234
+ - エラーハンドリングを適切に行う
235
+ - パフォーマンスを考慮した実装
236
+ - セキュリティに配慮すること
237
+
238
+ よろしくお願いします。`;
239
+
240
+ const callToolHandler = handlers.get('callTool')!;
241
+ const result = await callToolHandler!({
242
+ params: {
243
+ name: 'claude_code',
244
+ arguments: {
245
+ prompt: longJapanesePrompt,
246
+ workFolder: '/tmp',
247
+ model: 'sonnet'
248
+ }
249
+ }
250
+ });
251
+
252
+ // Verify PID is returned
253
+ const response = JSON.parse(result.content[0].text);
254
+ expect(response.pid).toBe(12361);
255
+
256
+ // Verify spawn was called with the complete long prompt
257
+ expect(mockSpawn).toHaveBeenCalledWith(
258
+ expect.any(String),
259
+ expect.arrayContaining(['-p', longJapanesePrompt]),
260
+ expect.any(Object)
261
+ );
262
+
263
+ // Check list_claude_processes truncates long prompts correctly
264
+ const listResult = await callToolHandler!({
265
+ params: {
266
+ name: 'list_claude_processes',
267
+ arguments: {}
268
+ }
269
+ });
270
+
271
+ const processes = JSON.parse(listResult.content[0].text);
272
+ const process = processes.find((p: any) => p.pid === 12361);
273
+ expect(process.prompt).toHaveLength(103); // 100 chars + '...'
274
+ expect(process.prompt.endsWith('...')).toBe(true);
275
+ expect(process.prompt).toContain('タスク:ファイル管理システムの作成');
276
+ });
277
+
278
+ it('should handle prompts with special characters and escape sequences', async () => {
279
+ const { handlers } = await setupServer();
280
+
281
+ const mockProcess = new EventEmitter() as any;
282
+ mockProcess.pid = 12362;
283
+ mockProcess.stdout = new EventEmitter();
284
+ mockProcess.stderr = new EventEmitter();
285
+ mockProcess.kill = vi.fn();
286
+
287
+ mockSpawn.mockReturnValue(mockProcess);
288
+
289
+ // Test with various special characters
290
+ const specialPrompt = `特殊文字のテスト:
291
+ \t- タブ文字
292
+ \n- 明示的な改行
293
+ "ダブルクォート" と 'シングルクォート'
294
+ バックスラッシュ: \\
295
+ Unicodeテスト: 🎌 🗾 ✨
296
+ 環境変数風: $HOME と \${USER}`;
297
+
298
+ const callToolHandler = handlers.get('callTool')!;
299
+ const result = await callToolHandler!({
300
+ params: {
301
+ name: 'claude_code',
302
+ arguments: {
303
+ prompt: specialPrompt,
304
+ workFolder: '/tmp'
305
+ }
306
+ }
307
+ });
308
+
309
+ // Verify PID is returned
310
+ const response = JSON.parse(result.content[0].text);
311
+ expect(response.pid).toBe(12362);
312
+
313
+ // Verify spawn was called with the special characters intact
314
+ expect(mockSpawn).toHaveBeenCalledWith(
315
+ expect.any(String),
316
+ expect.arrayContaining(['-p', specialPrompt]),
317
+ expect.any(Object)
318
+ );
319
+ });
320
+
321
+ it('should throw error if process fails to start', async () => {
322
+ const { handlers } = await setupServer();
323
+
324
+ const mockProcess = new EventEmitter() as any;
325
+ mockProcess.pid = undefined; // No PID means process failed to start
326
+ mockProcess.stdout = new EventEmitter();
327
+ mockProcess.stderr = new EventEmitter();
328
+
329
+ mockSpawn.mockReturnValue(mockProcess);
330
+
331
+ const callToolHandler = handlers.get('callTool')!;
332
+ await expect(callToolHandler!({
333
+ params: {
334
+ name: 'claude_code',
335
+ arguments: {
336
+ prompt: 'test prompt',
337
+ workFolder: '/tmp/test'
338
+ }
339
+ }
340
+ })).rejects.toThrow('Failed to start Claude CLI process');
341
+ });
342
+ });
343
+
344
+ describe('list_claude_processes tool', () => {
345
+ it('should list all processes', async () => {
346
+ const { handlers } = await setupServer();
347
+
348
+ // Start a process first
349
+ const mockProcess = new EventEmitter() as any;
350
+ mockProcess.pid = 12347;
351
+ mockProcess.stdout = new EventEmitter();
352
+ mockProcess.stderr = new EventEmitter();
353
+ mockProcess.kill = vi.fn();
354
+
355
+ mockSpawn.mockReturnValue(mockProcess);
356
+
357
+ const callToolHandler = handlers.get('callTool')!;
358
+
359
+ // Start a process
360
+ await callToolHandler!({
361
+ params: {
362
+ name: 'claude_code',
363
+ arguments: {
364
+ prompt: 'test prompt for listing',
365
+ workFolder: '/tmp',
366
+ model: 'sonnet'
367
+ }
368
+ }
369
+ });
370
+
371
+ // Simulate JSON output with session_id
372
+ const jsonOutput = {
373
+ session_id: 'list-test-session-789',
374
+ status: 'running'
375
+ };
376
+ mockProcess.stdout.emit('data', JSON.stringify(jsonOutput));
377
+
378
+ // List processes
379
+ const listResult = await callToolHandler!({
380
+ params: {
381
+ name: 'list_claude_processes',
382
+ arguments: {}
383
+ }
384
+ });
385
+
386
+ const processes = JSON.parse(listResult.content[0].text);
387
+ expect(processes).toHaveLength(1);
388
+ expect(processes[0].pid).toBe(12347);
389
+ expect(processes[0].status).toBe('running');
390
+ expect(processes[0].prompt).toContain('test prompt for listing');
391
+ expect(processes[0].model).toBe('sonnet');
392
+ expect(processes[0].session_id).toBe('list-test-session-789');
393
+ });
394
+
395
+ it('should truncate long prompts in list', async () => {
396
+ const { handlers } = await setupServer();
397
+
398
+ const mockProcess = new EventEmitter() as any;
399
+ mockProcess.pid = 12348;
400
+ mockProcess.stdout = new EventEmitter();
401
+ mockProcess.stderr = new EventEmitter();
402
+ mockProcess.kill = vi.fn();
403
+
404
+ mockSpawn.mockReturnValue(mockProcess);
405
+
406
+ const callToolHandler = handlers.get('callTool');
407
+
408
+ // Start a process with a very long prompt
409
+ const longPrompt = 'a'.repeat(150);
410
+ await callToolHandler!({
411
+ params: {
412
+ name: 'claude_code',
413
+ arguments: {
414
+ prompt: longPrompt,
415
+ workFolder: '/tmp'
416
+ }
417
+ }
418
+ });
419
+
420
+ // List processes
421
+ const listResult = await callToolHandler!({
422
+ params: {
423
+ name: 'list_claude_processes',
424
+ arguments: {}
425
+ }
426
+ });
427
+
428
+ const processes = JSON.parse(listResult.content[0].text);
429
+ expect(processes[0].prompt).toHaveLength(103); // 100 chars + '...'
430
+ expect(processes[0].prompt.endsWith('...')).toBe(true);
431
+ });
432
+ });
433
+
434
+ describe('get_claude_result tool', () => {
435
+ it('should get process output', async () => {
436
+ const { handlers } = await setupServer();
437
+
438
+ const mockProcess = new EventEmitter() as any;
439
+ mockProcess.pid = 12349;
440
+ mockProcess.stdout = new EventEmitter();
441
+ mockProcess.stderr = new EventEmitter();
442
+ mockProcess.kill = vi.fn();
443
+
444
+ mockSpawn.mockReturnValue(mockProcess);
445
+
446
+ const callToolHandler = handlers.get('callTool')!;
447
+
448
+ // Start a process
449
+ await callToolHandler!({
450
+ params: {
451
+ name: 'claude_code',
452
+ arguments: {
453
+ prompt: 'test prompt',
454
+ workFolder: '/tmp'
455
+ }
456
+ }
457
+ });
458
+
459
+ // Simulate JSON output from Claude CLI
460
+ const claudeJsonOutput = {
461
+ session_id: 'test-session-123',
462
+ status: 'success',
463
+ message: 'Task completed'
464
+ };
465
+ mockProcess.stdout.emit('data', JSON.stringify(claudeJsonOutput));
466
+ mockProcess.stderr.emit('data', 'Warning from stderr\n');
467
+
468
+ // Get result
469
+ const result = await callToolHandler!({
470
+ params: {
471
+ name: 'get_claude_result',
472
+ arguments: {
473
+ pid: 12349
474
+ }
475
+ }
476
+ });
477
+
478
+ const processInfo = JSON.parse(result.content[0].text);
479
+ expect(processInfo.pid).toBe(12349);
480
+ expect(processInfo.status).toBe('running');
481
+ expect(processInfo.claudeOutput).toEqual(claudeJsonOutput);
482
+ expect(processInfo.session_id).toBe('test-session-123');
483
+ });
484
+
485
+ it('should show completed status when process exits', async () => {
486
+ const { handlers } = await setupServer();
487
+
488
+ const mockProcess = new EventEmitter() as any;
489
+ mockProcess.pid = 12350;
490
+ mockProcess.stdout = new EventEmitter();
491
+ mockProcess.stderr = new EventEmitter();
492
+ mockProcess.kill = vi.fn();
493
+
494
+ mockSpawn.mockReturnValue(mockProcess);
495
+
496
+ const callToolHandler = handlers.get('callTool')!;
497
+
498
+ // Start a process
499
+ await callToolHandler!({
500
+ params: {
501
+ name: 'claude_code',
502
+ arguments: {
503
+ prompt: 'test prompt',
504
+ workFolder: '/tmp'
505
+ }
506
+ }
507
+ });
508
+
509
+ // Simulate process completion with JSON output
510
+ const completedJsonOutput = {
511
+ session_id: 'completed-session-456',
512
+ status: 'completed',
513
+ files_created: ['test.txt'],
514
+ summary: 'Created test file successfully'
515
+ };
516
+ mockProcess.stdout.emit('data', JSON.stringify(completedJsonOutput));
517
+ mockProcess.emit('close', 0);
518
+
519
+ // Get result
520
+ const result = await callToolHandler!({
521
+ params: {
522
+ name: 'get_claude_result',
523
+ arguments: {
524
+ pid: 12350
525
+ }
526
+ }
527
+ });
528
+
529
+ const processInfo = JSON.parse(result.content[0].text);
530
+ expect(processInfo.status).toBe('completed');
531
+ expect(processInfo.exitCode).toBe(0);
532
+ expect(processInfo.claudeOutput).toEqual(completedJsonOutput);
533
+ expect(processInfo.session_id).toBe('completed-session-456');
534
+ });
535
+
536
+ it('should throw error for non-existent PID', async () => {
537
+ const { handlers } = await setupServer();
538
+
539
+ const callToolHandler = handlers.get('callTool');
540
+
541
+ await expect(callToolHandler!({
542
+ params: {
543
+ name: 'get_claude_result',
544
+ arguments: {
545
+ pid: 99999
546
+ }
547
+ }
548
+ })).rejects.toThrow('Process with PID 99999 not found');
549
+ });
550
+
551
+ it('should throw error for invalid PID parameter', async () => {
552
+ const { handlers } = await setupServer();
553
+
554
+ const callToolHandler = handlers.get('callTool');
555
+
556
+ await expect(callToolHandler!({
557
+ params: {
558
+ name: 'get_claude_result',
559
+ arguments: {
560
+ pid: 'not-a-number'
561
+ }
562
+ }
563
+ })).rejects.toThrow('Missing or invalid required parameter: pid');
564
+ });
565
+
566
+ it('should handle non-JSON output gracefully', async () => {
567
+ const { handlers } = await setupServer();
568
+
569
+ const mockProcess = new EventEmitter() as any;
570
+ mockProcess.pid = 12355;
571
+ mockProcess.stdout = new EventEmitter();
572
+ mockProcess.stderr = new EventEmitter();
573
+ mockProcess.kill = vi.fn();
574
+
575
+ mockSpawn.mockReturnValue(mockProcess);
576
+
577
+ const callToolHandler = handlers.get('callTool')!;
578
+
579
+ // Start a process
580
+ await callToolHandler!({
581
+ params: {
582
+ name: 'claude_code',
583
+ arguments: {
584
+ prompt: 'test prompt',
585
+ workFolder: '/tmp'
586
+ }
587
+ }
588
+ });
589
+
590
+ // Simulate non-JSON output
591
+ mockProcess.stdout.emit('data', 'This is plain text output, not JSON');
592
+ mockProcess.stderr.emit('data', 'Some error occurred');
593
+
594
+ // Get result
595
+ const result = await callToolHandler!({
596
+ params: {
597
+ name: 'get_claude_result',
598
+ arguments: {
599
+ pid: 12355
600
+ }
601
+ }
602
+ });
603
+
604
+ const processInfo = JSON.parse(result.content[0].text);
605
+ expect(processInfo.pid).toBe(12355);
606
+ expect(processInfo.status).toBe('running');
607
+ expect(processInfo.stdout).toBe('This is plain text output, not JSON');
608
+ expect(processInfo.stderr).toBe('Some error occurred');
609
+ expect(processInfo.claudeOutput).toBeUndefined();
610
+ expect(processInfo.session_id).toBeUndefined();
611
+ });
612
+ });
613
+
614
+ describe('kill_claude_process tool', () => {
615
+ it('should kill a running process', async () => {
616
+ const { handlers } = await setupServer();
617
+
618
+ const mockProcess = new EventEmitter() as any;
619
+ mockProcess.pid = 12351;
620
+ mockProcess.stdout = new EventEmitter();
621
+ mockProcess.stderr = new EventEmitter();
622
+ mockProcess.kill = vi.fn();
623
+
624
+ mockSpawn.mockReturnValue(mockProcess);
625
+
626
+ const callToolHandler = handlers.get('callTool')!;
627
+
628
+ // Start a process
629
+ await callToolHandler!({
630
+ params: {
631
+ name: 'claude_code',
632
+ arguments: {
633
+ prompt: 'test prompt',
634
+ workFolder: '/tmp'
635
+ }
636
+ }
637
+ });
638
+
639
+ // Kill the process
640
+ const killResult = await callToolHandler!({
641
+ params: {
642
+ name: 'kill_claude_process',
643
+ arguments: {
644
+ pid: 12351
645
+ }
646
+ }
647
+ });
648
+
649
+ const response = JSON.parse(killResult.content[0].text);
650
+ expect(response.status).toBe('terminated');
651
+ expect(response.message).toBe('Process terminated successfully');
652
+ expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
653
+ });
654
+
655
+ it('should handle already terminated process', async () => {
656
+ const { handlers } = await setupServer();
657
+
658
+ const mockProcess = new EventEmitter() as any;
659
+ mockProcess.pid = 12352;
660
+ mockProcess.stdout = new EventEmitter();
661
+ mockProcess.stderr = new EventEmitter();
662
+ mockProcess.kill = vi.fn();
663
+
664
+ mockSpawn.mockReturnValue(mockProcess);
665
+
666
+ const callToolHandler = handlers.get('callTool');
667
+
668
+ // Start and complete a process
669
+ await callToolHandler!({
670
+ params: {
671
+ name: 'claude_code',
672
+ arguments: {
673
+ prompt: 'test prompt',
674
+ workFolder: '/tmp'
675
+ }
676
+ }
677
+ });
678
+
679
+ // Simulate process completion
680
+ mockProcess.emit('close', 0);
681
+
682
+ // Try to kill the already completed process
683
+ const killResult = await callToolHandler!({
684
+ params: {
685
+ name: 'kill_claude_process',
686
+ arguments: {
687
+ pid: 12352
688
+ }
689
+ }
690
+ });
691
+
692
+ const response = JSON.parse(killResult.content[0].text);
693
+ expect(response.status).toBe('completed');
694
+ expect(response.message).toBe('Process already terminated');
695
+ expect(mockProcess.kill).not.toHaveBeenCalled();
696
+ });
697
+
698
+ it('should throw error for non-existent PID', async () => {
699
+ const { handlers } = await setupServer();
700
+
701
+ const callToolHandler = handlers.get('callTool');
702
+
703
+ await expect(callToolHandler!({
704
+ params: {
705
+ name: 'kill_claude_process',
706
+ arguments: {
707
+ pid: 99999
708
+ }
709
+ }
710
+ })).rejects.toThrow('Process with PID 99999 not found');
711
+ });
712
+ });
713
+
714
+ describe('Tool routing', () => {
715
+ it('should throw error for unknown tool', async () => {
716
+ const { handlers } = await setupServer();
717
+
718
+ const callToolHandler = handlers.get('callTool');
719
+
720
+ await expect(callToolHandler!({
721
+ params: {
722
+ name: 'unknown_tool',
723
+ arguments: {}
724
+ }
725
+ })).rejects.toThrow('Tool unknown_tool not found');
726
+ });
727
+ });
728
+
729
+ describe('Process error handling', () => {
730
+ it('should handle process errors', async () => {
731
+ const { handlers } = await setupServer();
732
+
733
+ const mockProcess = new EventEmitter() as any;
734
+ mockProcess.pid = 12353;
735
+ mockProcess.stdout = new EventEmitter();
736
+ mockProcess.stderr = new EventEmitter();
737
+ mockProcess.kill = vi.fn();
738
+
739
+ mockSpawn.mockReturnValue(mockProcess);
740
+
741
+ const callToolHandler = handlers.get('callTool')!;
742
+
743
+ // Start a process
744
+ await callToolHandler!({
745
+ params: {
746
+ name: 'claude_code',
747
+ arguments: {
748
+ prompt: 'test prompt',
749
+ workFolder: '/tmp'
750
+ }
751
+ }
752
+ });
753
+
754
+ // Simulate process error
755
+ mockProcess.emit('error', new Error('spawn error'));
756
+
757
+ // Get result to check error was recorded
758
+ const result = await callToolHandler!({
759
+ params: {
760
+ name: 'get_claude_result',
761
+ arguments: {
762
+ pid: 12353
763
+ }
764
+ }
765
+ });
766
+
767
+ const processInfo = JSON.parse(result.content[0].text);
768
+ expect(processInfo.status).toBe('failed');
769
+ expect(processInfo.stderr).toContain('Process error: spawn error');
770
+ });
771
+ });
772
+ });