ai-cli-mcp 2.3.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.
Files changed (36) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/dist/__tests__/e2e.test.js +232 -0
  3. package/dist/__tests__/edge-cases.test.js +135 -0
  4. package/dist/__tests__/error-cases.test.js +291 -0
  5. package/dist/__tests__/mocks.js +32 -0
  6. package/dist/__tests__/model-alias.test.js +36 -0
  7. package/dist/__tests__/process-management.test.js +630 -0
  8. package/dist/__tests__/server.test.js +681 -0
  9. package/dist/__tests__/setup.js +11 -0
  10. package/dist/__tests__/utils/claude-mock.js +80 -0
  11. package/dist/__tests__/utils/mcp-client.js +104 -0
  12. package/dist/__tests__/utils/persistent-mock.js +25 -0
  13. package/dist/__tests__/utils/test-helpers.js +11 -0
  14. package/dist/__tests__/validation.test.js +235 -0
  15. package/dist/__tests__/version-print.test.js +69 -0
  16. package/dist/__tests__/wait.test.js +229 -0
  17. package/dist/parsers.js +68 -0
  18. package/dist/server.js +774 -0
  19. package/package.json +1 -1
  20. package/src/__tests__/e2e.test.ts +16 -24
  21. package/src/__tests__/error-cases.test.ts +8 -17
  22. package/src/__tests__/process-management.test.ts +22 -24
  23. package/src/__tests__/validation.test.ts +58 -36
  24. package/src/server.ts +6 -2
  25. package/data/rooms/refactor-haiku-alias-main/messages.jsonl +0 -5
  26. package/data/rooms/refactor-haiku-alias-main/presence.json +0 -20
  27. package/data/rooms.json +0 -10
  28. package/hello.txt +0 -3
  29. package/implementation-log.md +0 -110
  30. package/implementation-plan.md +0 -189
  31. package/investigation-report.md +0 -135
  32. package/quality-score.json +0 -47
  33. package/refactoring-requirements.md +0 -25
  34. package/review-report.md +0 -132
  35. package/test-results.md +0 -119
  36. package/xx.txt +0 -1
@@ -0,0 +1,681 @@
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 { resolve as pathResolve } from 'node:path';
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { EventEmitter } from 'node:events';
8
+ // Mock dependencies
9
+ vi.mock('node:child_process');
10
+ vi.mock('node:fs');
11
+ vi.mock('node:os');
12
+ vi.mock('node:path', () => ({
13
+ resolve: vi.fn((path) => path),
14
+ join: vi.fn((...args) => args.join('/')),
15
+ isAbsolute: vi.fn((path) => path.startsWith('/'))
16
+ }));
17
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
18
+ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
19
+ ListToolsRequestSchema: { name: 'listTools' },
20
+ CallToolRequestSchema: { name: 'callTool' },
21
+ ErrorCode: {
22
+ InternalError: 'InternalError',
23
+ MethodNotFound: 'MethodNotFound',
24
+ InvalidParams: 'InvalidParams'
25
+ },
26
+ McpError: class extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.code = code;
31
+ }
32
+ }
33
+ }));
34
+ vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
35
+ Server: vi.fn().mockImplementation(function () {
36
+ this.setRequestHandler = vi.fn();
37
+ this.connect = vi.fn();
38
+ this.close = vi.fn();
39
+ this.onerror = undefined;
40
+ return this;
41
+ }),
42
+ }));
43
+ // Mock package.json
44
+ vi.mock('../../package.json', () => ({
45
+ default: { version: '1.0.0-test' }
46
+ }));
47
+ // Re-import after mocks
48
+ const mockExistsSync = vi.mocked(existsSync);
49
+ const mockSpawn = vi.mocked(spawn);
50
+ const mockHomedir = vi.mocked(homedir);
51
+ const mockPathResolve = vi.mocked(pathResolve);
52
+ // Module loading will happen in tests
53
+ describe('ClaudeCodeServer Unit Tests', () => {
54
+ let consoleErrorSpy;
55
+ let consoleWarnSpy;
56
+ let originalEnv;
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ vi.resetModules();
60
+ vi.unmock('../server.js');
61
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
62
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
63
+ originalEnv = { ...process.env };
64
+ // Reset env
65
+ process.env = { ...originalEnv };
66
+ });
67
+ afterEach(() => {
68
+ consoleErrorSpy.mockRestore();
69
+ consoleWarnSpy.mockRestore();
70
+ process.env = originalEnv;
71
+ });
72
+ describe('debugLog function', () => {
73
+ it('should log when debug mode is enabled', async () => {
74
+ process.env.MCP_CLAUDE_DEBUG = 'true';
75
+ const module = await import('../server.js');
76
+ // @ts-ignore - accessing private function for testing
77
+ const { debugLog } = module;
78
+ debugLog('Test message');
79
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Test message');
80
+ });
81
+ it('should not log when debug mode is disabled', async () => {
82
+ // Reset modules to clear cache
83
+ vi.resetModules();
84
+ consoleErrorSpy.mockClear();
85
+ process.env.MCP_CLAUDE_DEBUG = 'false';
86
+ const module = await import('../server.js');
87
+ // @ts-ignore
88
+ const { debugLog } = module;
89
+ debugLog('Test message');
90
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
91
+ });
92
+ });
93
+ describe('findClaudeCli function', () => {
94
+ it('should return local path when it exists', async () => {
95
+ mockHomedir.mockReturnValue('/home/user');
96
+ mockExistsSync.mockImplementation((path) => {
97
+ // Mock returns true for real CLI path
98
+ if (path === '/home/user/.claude/local/claude')
99
+ return true;
100
+ return false;
101
+ });
102
+ const module = await import('../server.js');
103
+ // @ts-ignore
104
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
105
+ const result = findClaudeCli();
106
+ expect(result).toBe('/home/user/.claude/local/claude');
107
+ });
108
+ it('should fallback to PATH when local does not exist', async () => {
109
+ mockHomedir.mockReturnValue('/home/user');
110
+ mockExistsSync.mockReturnValue(false);
111
+ const module = await import('../server.js');
112
+ // @ts-ignore
113
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
114
+ const result = findClaudeCli();
115
+ expect(result).toBe('claude');
116
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Claude CLI not found at ~/.claude/local/claude'));
117
+ });
118
+ it('should use custom name from CLAUDE_CLI_NAME', async () => {
119
+ process.env.CLAUDE_CLI_NAME = 'my-claude';
120
+ mockHomedir.mockReturnValue('/home/user');
121
+ mockExistsSync.mockReturnValue(false);
122
+ const module = await import('../server.js');
123
+ // @ts-ignore
124
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
125
+ const result = findClaudeCli();
126
+ expect(result).toBe('my-claude');
127
+ });
128
+ it('should use absolute path from CLAUDE_CLI_NAME', async () => {
129
+ process.env.CLAUDE_CLI_NAME = '/absolute/path/to/claude';
130
+ const module = await import('../server.js');
131
+ // @ts-ignore
132
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
133
+ const result = findClaudeCli();
134
+ expect(result).toBe('/absolute/path/to/claude');
135
+ });
136
+ it('should throw error for relative paths in CLAUDE_CLI_NAME', async () => {
137
+ process.env.CLAUDE_CLI_NAME = './relative/path/claude';
138
+ const module = await import('../server.js');
139
+ // @ts-ignore
140
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
141
+ expect(() => findClaudeCli()).toThrow('Invalid CLAUDE_CLI_NAME: Relative paths are not allowed');
142
+ });
143
+ it('should throw error for paths with ../ in CLAUDE_CLI_NAME', async () => {
144
+ process.env.CLAUDE_CLI_NAME = '../relative/path/claude';
145
+ const module = await import('../server.js');
146
+ // @ts-ignore
147
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
148
+ expect(() => findClaudeCli()).toThrow('Invalid CLAUDE_CLI_NAME: Relative paths are not allowed');
149
+ });
150
+ });
151
+ describe('spawnAsync function', () => {
152
+ let mockProcess;
153
+ beforeEach(() => {
154
+ // Create a mock process
155
+ mockProcess = new EventEmitter();
156
+ mockProcess.stdout = new EventEmitter();
157
+ mockProcess.stderr = new EventEmitter();
158
+ mockProcess.stdout.on = vi.fn((event, handler) => {
159
+ mockProcess.stdout[event] = handler;
160
+ });
161
+ mockProcess.stderr.on = vi.fn((event, handler) => {
162
+ mockProcess.stderr[event] = handler;
163
+ });
164
+ mockSpawn.mockReturnValue(mockProcess);
165
+ });
166
+ it('should execute command successfully', async () => {
167
+ const module = await import('../server.js');
168
+ // @ts-ignore
169
+ const { spawnAsync } = module;
170
+ // mockProcess is already defined in the outer scope
171
+ // Start the async operation
172
+ const promise = spawnAsync('echo', ['test']);
173
+ // Simulate successful execution
174
+ setTimeout(() => {
175
+ mockProcess.stdout['data']('test output');
176
+ mockProcess.stderr['data']('');
177
+ mockProcess.emit('close', 0);
178
+ }, 10);
179
+ const result = await promise;
180
+ expect(result).toEqual({
181
+ stdout: 'test output',
182
+ stderr: ''
183
+ });
184
+ });
185
+ it('should handle command failure', async () => {
186
+ const module = await import('../server.js');
187
+ // @ts-ignore
188
+ const { spawnAsync } = module;
189
+ // mockProcess is already defined in the outer scope
190
+ // Start the async operation
191
+ const promise = spawnAsync('false', []);
192
+ // Simulate failed execution
193
+ setTimeout(() => {
194
+ mockProcess.stderr['data']('error output');
195
+ mockProcess.emit('close', 1);
196
+ }, 10);
197
+ await expect(promise).rejects.toThrow('Command failed with exit code 1');
198
+ });
199
+ it('should handle spawn error', async () => {
200
+ const module = await import('../server.js');
201
+ // @ts-ignore
202
+ const { spawnAsync } = module;
203
+ // mockProcess is already defined in the outer scope
204
+ // Start the async operation
205
+ const promise = spawnAsync('nonexistent', []);
206
+ // Simulate spawn error
207
+ setTimeout(() => {
208
+ const error = new Error('spawn error');
209
+ error.code = 'ENOENT';
210
+ error.path = 'nonexistent';
211
+ error.syscall = 'spawn';
212
+ mockProcess.emit('error', error);
213
+ }, 10);
214
+ await expect(promise).rejects.toThrow('Spawn error');
215
+ });
216
+ it('should respect timeout option', async () => {
217
+ const module = await import('../server.js');
218
+ // @ts-ignore
219
+ const { spawnAsync } = module;
220
+ const result = spawnAsync('sleep', ['10'], { timeout: 100 });
221
+ expect(mockSpawn).toHaveBeenCalledWith('sleep', ['10'], expect.objectContaining({
222
+ timeout: 100
223
+ }));
224
+ });
225
+ it('should use provided cwd option', async () => {
226
+ const module = await import('../server.js');
227
+ // @ts-ignore
228
+ const { spawnAsync } = module;
229
+ const result = spawnAsync('ls', [], { cwd: '/tmp' });
230
+ expect(mockSpawn).toHaveBeenCalledWith('ls', [], expect.objectContaining({
231
+ cwd: '/tmp'
232
+ }));
233
+ });
234
+ });
235
+ describe('ClaudeCodeServer class', () => {
236
+ it('should initialize with correct settings', async () => {
237
+ mockHomedir.mockReturnValue('/home/user');
238
+ mockExistsSync.mockReturnValue(true);
239
+ // Set up Server mock before resetting modules
240
+ vi.mocked(Server).mockImplementation(function () {
241
+ this.setRequestHandler = vi.fn();
242
+ this.connect = vi.fn();
243
+ this.close = vi.fn();
244
+ this.onerror = undefined;
245
+ return this;
246
+ });
247
+ const module = await import('../server.js');
248
+ // @ts-ignore
249
+ const { ClaudeCodeServer } = module;
250
+ const server = new ClaudeCodeServer();
251
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('[Setup] Using Claude CLI command/path:'));
252
+ });
253
+ it('should set up tool handlers', async () => {
254
+ mockHomedir.mockReturnValue('/home/user');
255
+ mockExistsSync.mockReturnValue(true);
256
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
257
+ const mockSetRequestHandler = vi.fn();
258
+ vi.mocked(Server).mockImplementation(function () {
259
+ this.setRequestHandler = mockSetRequestHandler;
260
+ this.connect = vi.fn();
261
+ this.close = vi.fn();
262
+ this.onerror = undefined;
263
+ return this;
264
+ });
265
+ const module = await import('../server.js');
266
+ // @ts-ignore
267
+ const { ClaudeCodeServer } = module;
268
+ const server = new ClaudeCodeServer();
269
+ expect(mockSetRequestHandler).toHaveBeenCalled();
270
+ });
271
+ it('should set up error handler', async () => {
272
+ mockHomedir.mockReturnValue('/home/user');
273
+ mockExistsSync.mockReturnValue(true);
274
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
275
+ let errorHandler = null;
276
+ vi.mocked(Server).mockImplementation(function () {
277
+ this.setRequestHandler = vi.fn();
278
+ this.connect = vi.fn();
279
+ this.close = vi.fn();
280
+ Object.defineProperty(this, 'onerror', {
281
+ get() { return errorHandler; },
282
+ set(handler) { errorHandler = handler; },
283
+ enumerable: true,
284
+ configurable: true
285
+ });
286
+ return this;
287
+ });
288
+ const module = await import('../server.js');
289
+ // @ts-ignore
290
+ const { ClaudeCodeServer } = module;
291
+ const server = new ClaudeCodeServer();
292
+ // Test error handler
293
+ errorHandler(new Error('Test error'));
294
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[Error]', expect.any(Error));
295
+ });
296
+ it('should handle SIGINT', async () => {
297
+ mockHomedir.mockReturnValue('/home/user');
298
+ mockExistsSync.mockReturnValue(true);
299
+ // Set up Server mock first
300
+ vi.mocked(Server).mockImplementation(function () {
301
+ this.setRequestHandler = vi.fn();
302
+ this.connect = vi.fn();
303
+ this.close = vi.fn();
304
+ this.onerror = undefined;
305
+ return this;
306
+ });
307
+ const module = await import('../server.js');
308
+ // @ts-ignore
309
+ const { ClaudeCodeServer } = module;
310
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
311
+ const server = new ClaudeCodeServer();
312
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
313
+ // Emit SIGINT
314
+ const sigintHandler = process.listeners('SIGINT').slice(-1)[0];
315
+ await sigintHandler();
316
+ expect(mockServerInstance.close).toHaveBeenCalled();
317
+ expect(exitSpy).toHaveBeenCalledWith(0);
318
+ exitSpy.mockRestore();
319
+ });
320
+ });
321
+ describe('Tool handler implementation', () => {
322
+ // Define setupServerMock for this describe block
323
+ let errorHandler = null;
324
+ function setupServerMock() {
325
+ errorHandler = null;
326
+ vi.mocked(Server).mockImplementation(function () {
327
+ this.setRequestHandler = vi.fn();
328
+ this.connect = vi.fn();
329
+ this.close = vi.fn();
330
+ Object.defineProperty(this, 'onerror', {
331
+ get() { return errorHandler; },
332
+ set(handler) { errorHandler = handler; },
333
+ enumerable: true,
334
+ configurable: true
335
+ });
336
+ return this;
337
+ });
338
+ }
339
+ it('should handle ListToolsRequest', async () => {
340
+ mockHomedir.mockReturnValue('/home/user');
341
+ mockExistsSync.mockReturnValue(true);
342
+ // Use the setupServerMock function from the beginning of the file
343
+ setupServerMock();
344
+ const module = await import('../server.js');
345
+ // @ts-ignore
346
+ const { ClaudeCodeServer } = module;
347
+ const server = new ClaudeCodeServer();
348
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
349
+ // Find the ListToolsRequest handler
350
+ const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'listTools');
351
+ expect(listToolsCall).toBeDefined();
352
+ // Test the handler
353
+ const handler = listToolsCall[1];
354
+ const result = await handler();
355
+ expect(result.tools).toHaveLength(6);
356
+ expect(result.tools[0].name).toBe('run');
357
+ expect(result.tools[0].description).toContain('AI Agent Runner');
358
+ expect(result.tools[1].name).toBe('list_processes');
359
+ expect(result.tools[2].name).toBe('get_result');
360
+ expect(result.tools[3].name).toBe('wait');
361
+ expect(result.tools[4].name).toBe('kill_process');
362
+ expect(result.tools[5].name).toBe('cleanup_processes');
363
+ });
364
+ it('should handle CallToolRequest', async () => {
365
+ mockHomedir.mockReturnValue('/home/user');
366
+ mockExistsSync.mockReturnValue(true);
367
+ // Set up Server mock
368
+ setupServerMock();
369
+ const module = await import('../server.js');
370
+ // @ts-ignore
371
+ const { ClaudeCodeServer } = module;
372
+ const server = new ClaudeCodeServer();
373
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
374
+ // Find the CallToolRequest handler
375
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
376
+ expect(callToolCall).toBeDefined();
377
+ // Create a mock process for the tool execution
378
+ const mockProcess = new EventEmitter();
379
+ mockProcess.pid = 12345;
380
+ mockProcess.stdout = new EventEmitter();
381
+ mockProcess.stderr = new EventEmitter();
382
+ mockProcess.stdout.on = vi.fn();
383
+ mockProcess.stderr.on = vi.fn();
384
+ mockProcess.kill = vi.fn();
385
+ mockSpawn.mockReturnValue(mockProcess);
386
+ // Test the handler
387
+ const handler = callToolCall[1];
388
+ const result = await handler({
389
+ params: {
390
+ name: 'run',
391
+ arguments: {
392
+ prompt: 'test prompt',
393
+ workFolder: '/tmp'
394
+ }
395
+ }
396
+ });
397
+ // run now returns PID immediately
398
+ expect(result.content[0].type).toBe('text');
399
+ const response = JSON.parse(result.content[0].text);
400
+ expect(response.pid).toBe(12345);
401
+ expect(response.status).toBe('started');
402
+ });
403
+ it('should require workFolder parameter', async () => {
404
+ mockHomedir.mockReturnValue('/home/user');
405
+ mockExistsSync.mockReturnValue(true);
406
+ // Set up Server mock
407
+ setupServerMock();
408
+ const module = await import('../server.js');
409
+ // @ts-ignore
410
+ const { ClaudeCodeServer } = module;
411
+ const server = new ClaudeCodeServer();
412
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
413
+ // Find the CallToolRequest handler
414
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
415
+ const handler = callToolCall[1];
416
+ // Test missing workFolder
417
+ try {
418
+ await handler({
419
+ params: {
420
+ name: 'run',
421
+ arguments: {
422
+ prompt: 'test'
423
+ }
424
+ }
425
+ });
426
+ expect.fail('Should have thrown');
427
+ }
428
+ catch (error) {
429
+ expect(error.message).toContain('Missing or invalid required parameter: workFolder');
430
+ }
431
+ });
432
+ it('should handle non-existent workFolder', async () => {
433
+ mockHomedir.mockReturnValue('/home/user');
434
+ mockExistsSync.mockImplementation((path) => {
435
+ // Make the CLI path exist but the workFolder not exist
436
+ if (String(path).includes('.claude'))
437
+ return true;
438
+ if (path === '/nonexistent')
439
+ return false;
440
+ return false;
441
+ });
442
+ // Set up Server mock
443
+ setupServerMock();
444
+ const module = await import('../server.js');
445
+ // @ts-ignore
446
+ const { ClaudeCodeServer } = module;
447
+ const server = new ClaudeCodeServer();
448
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
449
+ // Find the CallToolRequest handler
450
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
451
+ const handler = callToolCall[1];
452
+ // Should throw error for non-existent workFolder
453
+ try {
454
+ await handler({
455
+ params: {
456
+ name: 'run',
457
+ arguments: {
458
+ prompt: 'test',
459
+ workFolder: '/nonexistent'
460
+ }
461
+ }
462
+ });
463
+ expect.fail('Should have thrown');
464
+ }
465
+ catch (error) {
466
+ expect(error.message).toContain('Working folder does not exist');
467
+ }
468
+ });
469
+ it('should handle session_id parameter', async () => {
470
+ mockHomedir.mockReturnValue('/home/user');
471
+ mockExistsSync.mockReturnValue(true);
472
+ // Set up Server mock
473
+ setupServerMock();
474
+ const module = await import('../server.js');
475
+ // @ts-ignore
476
+ const { ClaudeCodeServer } = module;
477
+ const server = new ClaudeCodeServer();
478
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
479
+ // Find the CallToolRequest handler
480
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
481
+ const handler = callToolCall[1];
482
+ // Create mock process
483
+ const mockProcess = new EventEmitter();
484
+ mockProcess.pid = 12347;
485
+ mockProcess.stdout = new EventEmitter();
486
+ mockProcess.stderr = new EventEmitter();
487
+ mockProcess.stdout.on = vi.fn();
488
+ mockProcess.stderr.on = vi.fn();
489
+ mockProcess.kill = vi.fn();
490
+ mockSpawn.mockReturnValue(mockProcess);
491
+ const result = await handler({
492
+ params: {
493
+ name: 'run',
494
+ arguments: {
495
+ prompt: 'test prompt',
496
+ workFolder: '/tmp',
497
+ session_id: 'test-session-123'
498
+ }
499
+ }
500
+ });
501
+ // Verify spawn was called with -r flag
502
+ expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-r', 'test-session-123', '-p', 'test prompt']), expect.any(Object));
503
+ });
504
+ it('should handle prompt_file parameter', async () => {
505
+ mockHomedir.mockReturnValue('/home/user');
506
+ mockExistsSync.mockImplementation((path) => {
507
+ if (String(path).includes('.claude'))
508
+ return true;
509
+ if (path === '/tmp')
510
+ return true;
511
+ if (path === '/tmp/prompt.txt')
512
+ return true;
513
+ return false;
514
+ });
515
+ // Mock readFileSync
516
+ const readFileSyncMock = vi.fn().mockReturnValue('Content from file');
517
+ vi.doMock('node:fs', () => ({
518
+ existsSync: mockExistsSync,
519
+ readFileSync: readFileSyncMock
520
+ }));
521
+ // Set up Server mock
522
+ setupServerMock();
523
+ const module = await import('../server.js');
524
+ // @ts-ignore
525
+ const { ClaudeCodeServer } = module;
526
+ const server = new ClaudeCodeServer();
527
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
528
+ // Find the CallToolRequest handler
529
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
530
+ const handler = callToolCall[1];
531
+ // Create mock process
532
+ const mockProcess = new EventEmitter();
533
+ mockProcess.pid = 12348;
534
+ mockProcess.stdout = new EventEmitter();
535
+ mockProcess.stderr = new EventEmitter();
536
+ mockProcess.stdout.on = vi.fn();
537
+ mockProcess.stderr.on = vi.fn();
538
+ mockProcess.kill = vi.fn();
539
+ mockSpawn.mockReturnValue(mockProcess);
540
+ const result = await handler({
541
+ params: {
542
+ name: 'run',
543
+ arguments: {
544
+ prompt_file: '/tmp/prompt.txt',
545
+ workFolder: '/tmp'
546
+ }
547
+ }
548
+ });
549
+ // Verify file was read and spawn was called with content
550
+ expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['-p', 'Content from file']), expect.any(Object));
551
+ });
552
+ it('should resolve model aliases when calling run tool', async () => {
553
+ mockHomedir.mockReturnValue('/home/user');
554
+ mockExistsSync.mockReturnValue(true);
555
+ // Set up spawn mock to return a process
556
+ const mockProcess = new EventEmitter();
557
+ mockProcess.stdout = new EventEmitter();
558
+ mockProcess.stderr = new EventEmitter();
559
+ mockProcess.pid = 12345;
560
+ mockSpawn.mockReturnValue(mockProcess);
561
+ // Set up Server mock
562
+ setupServerMock();
563
+ const module = await import('../server.js');
564
+ // @ts-ignore
565
+ const { ClaudeCodeServer } = module;
566
+ const server = new ClaudeCodeServer();
567
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
568
+ // Find the CallToolRequest handler
569
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
570
+ const handler = callToolCall[1];
571
+ // Test with haiku alias
572
+ const result = await handler({
573
+ params: {
574
+ name: 'run',
575
+ arguments: {
576
+ prompt: 'test prompt',
577
+ workFolder: '/tmp',
578
+ model: 'haiku'
579
+ }
580
+ }
581
+ });
582
+ // Verify spawn was called with resolved model name
583
+ expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['--model', 'claude-3-5-haiku-20241022']), expect.any(Object));
584
+ // Verify PID is returned
585
+ expect(result.content[0].text).toContain('"pid": 12345');
586
+ });
587
+ it('should pass non-alias model names unchanged', async () => {
588
+ mockHomedir.mockReturnValue('/home/user');
589
+ mockExistsSync.mockReturnValue(true);
590
+ // Set up spawn mock to return a process
591
+ const mockProcess = new EventEmitter();
592
+ mockProcess.stdout = new EventEmitter();
593
+ mockProcess.stderr = new EventEmitter();
594
+ mockProcess.pid = 12346;
595
+ mockSpawn.mockReturnValue(mockProcess);
596
+ // Set up Server mock
597
+ setupServerMock();
598
+ const module = await import('../server.js');
599
+ // @ts-ignore
600
+ const { ClaudeCodeServer } = module;
601
+ const server = new ClaudeCodeServer();
602
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
603
+ // Find the CallToolRequest handler
604
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
605
+ const handler = callToolCall[1];
606
+ // Test with non-alias model name
607
+ const result = await handler({
608
+ params: {
609
+ name: 'run',
610
+ arguments: {
611
+ prompt: 'test prompt',
612
+ workFolder: '/tmp',
613
+ model: 'sonnet'
614
+ }
615
+ }
616
+ });
617
+ // Verify spawn was called with unchanged model name
618
+ expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining(['--model', 'sonnet']), expect.any(Object));
619
+ });
620
+ it('should reject when both prompt and prompt_file are provided', async () => {
621
+ mockHomedir.mockReturnValue('/home/user');
622
+ mockExistsSync.mockReturnValue(true);
623
+ // Set up Server mock
624
+ setupServerMock();
625
+ const module = await import('../server.js');
626
+ // @ts-ignore
627
+ const { ClaudeCodeServer } = module;
628
+ const server = new ClaudeCodeServer();
629
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
630
+ // Find the CallToolRequest handler
631
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
632
+ const handler = callToolCall[1];
633
+ // Test both parameters provided
634
+ try {
635
+ await handler({
636
+ params: {
637
+ name: 'run',
638
+ arguments: {
639
+ prompt: 'test prompt',
640
+ prompt_file: '/tmp/prompt.txt',
641
+ workFolder: '/tmp'
642
+ }
643
+ }
644
+ });
645
+ expect.fail('Should have thrown an error');
646
+ }
647
+ catch (error) {
648
+ expect(error.message).toContain('Cannot specify both prompt and prompt_file');
649
+ }
650
+ });
651
+ it('should reject when neither prompt nor prompt_file are provided', async () => {
652
+ mockHomedir.mockReturnValue('/home/user');
653
+ mockExistsSync.mockReturnValue(true);
654
+ // Set up Server mock
655
+ setupServerMock();
656
+ const module = await import('../server.js');
657
+ // @ts-ignore
658
+ const { ClaudeCodeServer } = module;
659
+ const server = new ClaudeCodeServer();
660
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
661
+ // Find the CallToolRequest handler
662
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
663
+ const handler = callToolCall[1];
664
+ // Test neither parameter provided
665
+ try {
666
+ await handler({
667
+ params: {
668
+ name: 'run',
669
+ arguments: {
670
+ workFolder: '/tmp'
671
+ }
672
+ }
673
+ });
674
+ expect.fail('Should have thrown an error');
675
+ }
676
+ catch (error) {
677
+ expect(error.message).toContain('Either prompt or prompt_file must be provided');
678
+ }
679
+ });
680
+ });
681
+ });