ai-cli-mcp 2.3.0 → 2.3.2

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 (39) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/dist/__tests__/e2e.test.js +225 -0
  3. package/dist/__tests__/edge-cases.test.js +127 -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 +121 -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 +65 -0
  16. package/dist/__tests__/wait.test.js +229 -0
  17. package/dist/parsers.js +68 -0
  18. package/dist/server.js +772 -0
  19. package/package.json +1 -1
  20. package/src/__tests__/e2e.test.ts +19 -34
  21. package/src/__tests__/edge-cases.test.ts +5 -14
  22. package/src/__tests__/error-cases.test.ts +8 -17
  23. package/src/__tests__/process-management.test.ts +22 -24
  24. package/src/__tests__/utils/mcp-client.ts +30 -0
  25. package/src/__tests__/validation.test.ts +58 -36
  26. package/src/__tests__/version-print.test.ts +5 -10
  27. package/src/server.ts +5 -3
  28. package/data/rooms/refactor-haiku-alias-main/messages.jsonl +0 -5
  29. package/data/rooms/refactor-haiku-alias-main/presence.json +0 -20
  30. package/data/rooms.json +0 -10
  31. package/hello.txt +0 -3
  32. package/implementation-log.md +0 -110
  33. package/implementation-plan.md +0 -189
  34. package/investigation-report.md +0 -135
  35. package/quality-score.json +0 -47
  36. package/refactoring-requirements.md +0 -25
  37. package/review-report.md +0 -132
  38. package/test-results.md +0 -119
  39. package/xx.txt +0 -1
@@ -14,7 +14,8 @@
14
14
  "mcp__chat__agent_communication_get_messages",
15
15
  "mcp__ccm__list_claude_processes",
16
16
  "Bash(gemini:*)",
17
- "WebSearch"
17
+ "WebSearch",
18
+ "Bash(npm pack:*)"
18
19
  ],
19
20
  "deny": []
20
21
  }
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
2
+ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createTestClient } from './utils/mcp-client.js';
6
+ import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
7
+ describe('Claude Code MCP E2E Tests', () => {
8
+ let client;
9
+ let testDir;
10
+ beforeEach(async () => {
11
+ // Ensure mock exists
12
+ await getSharedMock();
13
+ // Create a temporary directory for test files
14
+ testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
15
+ client = createTestClient();
16
+ await client.connect();
17
+ });
18
+ afterEach(async () => {
19
+ // Disconnect client
20
+ await client.disconnect();
21
+ // Clean up test directory
22
+ rmSync(testDir, { recursive: true, force: true });
23
+ });
24
+ afterAll(async () => {
25
+ // Only cleanup mock at the very end
26
+ await cleanupSharedMock();
27
+ });
28
+ describe('Tool Registration', () => {
29
+ it('should register run tool', async () => {
30
+ const tools = await client.listTools();
31
+ expect(tools).toHaveLength(6);
32
+ const claudeCodeTool = tools.find((t) => t.name === 'run');
33
+ expect(claudeCodeTool).toEqual({
34
+ name: 'run',
35
+ description: expect.stringContaining('AI Agent Runner'),
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ prompt: {
40
+ type: 'string',
41
+ description: expect.stringContaining('Either this or prompt_file is required'),
42
+ },
43
+ prompt_file: {
44
+ type: 'string',
45
+ description: expect.stringContaining('Path to a file containing the prompt'),
46
+ },
47
+ workFolder: {
48
+ type: 'string',
49
+ description: expect.stringContaining('working directory'),
50
+ },
51
+ model: {
52
+ type: 'string',
53
+ description: expect.stringContaining('sonnet'),
54
+ },
55
+ session_id: {
56
+ type: 'string',
57
+ description: expect.stringContaining('session ID'),
58
+ },
59
+ },
60
+ required: ['workFolder'],
61
+ },
62
+ });
63
+ // Verify other tools exist
64
+ expect(tools.some((t) => t.name === 'list_processes')).toBe(true);
65
+ expect(tools.some((t) => t.name === 'get_result')).toBe(true);
66
+ expect(tools.some((t) => t.name === 'kill_process')).toBe(true);
67
+ });
68
+ });
69
+ describe('Basic Operations', () => {
70
+ it('should execute a simple prompt', async () => {
71
+ const response = await client.callTool('run', {
72
+ prompt: 'create a file called test.txt with content "Hello World"',
73
+ workFolder: testDir,
74
+ });
75
+ expect(response).toEqual([{
76
+ type: 'text',
77
+ text: expect.stringContaining('successfully'),
78
+ }]);
79
+ });
80
+ it('should handle process management correctly', async () => {
81
+ // run now returns a PID immediately
82
+ const response = await client.callTool('run', {
83
+ prompt: 'error',
84
+ workFolder: testDir,
85
+ });
86
+ expect(response).toEqual([{
87
+ type: 'text',
88
+ text: expect.stringContaining('pid'),
89
+ }]);
90
+ // Extract PID from response
91
+ const responseText = response[0].text;
92
+ const pidMatch = responseText.match(/"pid":\s*(\d+)/);
93
+ expect(pidMatch).toBeTruthy();
94
+ });
95
+ it('should reject missing workFolder', async () => {
96
+ await expect(client.callTool('run', {
97
+ prompt: 'List files in current directory',
98
+ })).rejects.toThrow(/workFolder/i);
99
+ });
100
+ });
101
+ describe('Working Directory Handling', () => {
102
+ it('should respect custom working directory', async () => {
103
+ const response = await client.callTool('run', {
104
+ prompt: 'Show current working directory',
105
+ workFolder: testDir,
106
+ });
107
+ expect(response).toBeTruthy();
108
+ });
109
+ it('should reject non-existent working directory', async () => {
110
+ const nonExistentDir = join(testDir, 'non-existent');
111
+ await expect(client.callTool('run', {
112
+ prompt: 'Test prompt',
113
+ workFolder: nonExistentDir,
114
+ })).rejects.toThrow(/does not exist/i);
115
+ });
116
+ });
117
+ describe('Timeout Handling', () => {
118
+ it('should respect timeout settings', async () => {
119
+ // This would require modifying the mock to simulate a long-running command
120
+ // Since we're testing locally, we'll skip the actual timeout test
121
+ expect(true).toBe(true);
122
+ });
123
+ });
124
+ describe('Model Alias Handling', () => {
125
+ it('should resolve haiku alias when calling run', async () => {
126
+ const response = await client.callTool('run', {
127
+ prompt: 'Test with haiku model',
128
+ workFolder: testDir,
129
+ model: 'haiku'
130
+ });
131
+ expect(response).toEqual([{
132
+ type: 'text',
133
+ text: expect.stringContaining('pid'),
134
+ }]);
135
+ // Extract PID from response
136
+ const responseText = response[0].text;
137
+ const pidMatch = responseText.match(/"pid":\s*(\d+)/);
138
+ expect(pidMatch).toBeTruthy();
139
+ // Get the PID and check the process using get_result
140
+ const pid = parseInt(pidMatch[1]);
141
+ const result = await client.callTool('get_result', { pid });
142
+ const resultText = result[0].text;
143
+ const processData = JSON.parse(resultText);
144
+ // Verify that the model was set correctly
145
+ expect(processData.model).toBe('haiku');
146
+ });
147
+ it('should pass non-alias model names unchanged', async () => {
148
+ const response = await client.callTool('run', {
149
+ prompt: 'Test with sonnet model',
150
+ workFolder: testDir,
151
+ model: 'sonnet'
152
+ });
153
+ expect(response).toEqual([{
154
+ type: 'text',
155
+ text: expect.stringContaining('pid'),
156
+ }]);
157
+ // Extract PID
158
+ const responseText = response[0].text;
159
+ const pidMatch = responseText.match(/"pid":\s*(\d+)/);
160
+ const pid = parseInt(pidMatch[1]);
161
+ // Check the process using get_result
162
+ const result = await client.callTool('get_result', { pid });
163
+ const resultText = result[0].text;
164
+ const processData = JSON.parse(resultText);
165
+ // The model should be unchanged
166
+ expect(processData.model).toBe('sonnet');
167
+ });
168
+ it('should work without specifying a model', async () => {
169
+ const response = await client.callTool('run', {
170
+ prompt: 'Test without model parameter',
171
+ workFolder: testDir
172
+ });
173
+ expect(response).toEqual([{
174
+ type: 'text',
175
+ text: expect.stringContaining('pid'),
176
+ }]);
177
+ });
178
+ });
179
+ describe('Debug Mode', () => {
180
+ it('should log debug information when enabled', async () => {
181
+ // Debug logs go to stderr, which we capture in the client
182
+ const response = await client.callTool('run', {
183
+ prompt: 'Debug test prompt',
184
+ workFolder: testDir,
185
+ });
186
+ expect(response).toBeTruthy();
187
+ });
188
+ });
189
+ });
190
+ describe('Integration Tests (Local Only)', () => {
191
+ let client;
192
+ let testDir;
193
+ beforeEach(async () => {
194
+ testDir = mkdtempSync(join(tmpdir(), 'claude-code-integration-'));
195
+ // Initialize client without mocks for real Claude testing
196
+ client = createTestClient({ claudeCliName: '' });
197
+ });
198
+ afterEach(async () => {
199
+ if (client) {
200
+ await client.disconnect();
201
+ }
202
+ rmSync(testDir, { recursive: true, force: true });
203
+ });
204
+ // These tests will only run locally when Claude is available
205
+ it.skip('should create a file with real Claude CLI', async () => {
206
+ await client.connect();
207
+ const response = await client.callTool('run', {
208
+ prompt: 'Create a file called hello.txt with content "Hello from Claude"',
209
+ workFolder: testDir,
210
+ });
211
+ const filePath = join(testDir, 'hello.txt');
212
+ expect(existsSync(filePath)).toBe(true);
213
+ expect(readFileSync(filePath, 'utf-8')).toContain('Hello from Claude');
214
+ });
215
+ it.skip('should handle git operations with real Claude CLI', async () => {
216
+ await client.connect();
217
+ // Initialize git repo
218
+ const response = await client.callTool('run', {
219
+ prompt: 'Initialize a git repository and create a README.md file',
220
+ workFolder: testDir,
221
+ });
222
+ expect(existsSync(join(testDir, '.git'))).toBe(true);
223
+ expect(existsSync(join(testDir, 'README.md'))).toBe(true);
224
+ });
225
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createTestClient } from './utils/mcp-client.js';
6
+ import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
7
+ describe('Claude Code Edge Cases', () => {
8
+ let client;
9
+ let testDir;
10
+ beforeEach(async () => {
11
+ // Ensure mock exists
12
+ await getSharedMock();
13
+ // Create test directory
14
+ testDir = mkdtempSync(join(tmpdir(), 'claude-code-edge-'));
15
+ client = createTestClient();
16
+ await client.connect();
17
+ });
18
+ afterEach(async () => {
19
+ await client.disconnect();
20
+ rmSync(testDir, { recursive: true, force: true });
21
+ });
22
+ afterAll(async () => {
23
+ // Cleanup mock only at the end
24
+ await cleanupSharedMock();
25
+ });
26
+ describe('Input Validation', () => {
27
+ it('should reject missing prompt', async () => {
28
+ await expect(client.callTool('run', {
29
+ workFolder: testDir,
30
+ })).rejects.toThrow(/prompt/i);
31
+ });
32
+ it('should reject invalid prompt type', async () => {
33
+ await expect(client.callTool('run', {
34
+ prompt: 123, // Should be string
35
+ workFolder: testDir,
36
+ })).rejects.toThrow();
37
+ });
38
+ it('should reject invalid workFolder type', async () => {
39
+ await expect(client.callTool('run', {
40
+ prompt: 'Test prompt',
41
+ workFolder: 123, // Should be string
42
+ })).rejects.toThrow(/workFolder/i);
43
+ });
44
+ it('should reject empty prompt', async () => {
45
+ await expect(client.callTool('run', {
46
+ prompt: '',
47
+ workFolder: testDir,
48
+ })).rejects.toThrow(/prompt/i);
49
+ });
50
+ });
51
+ describe('Special Characters', () => {
52
+ it.skip('should handle prompts with quotes', async () => {
53
+ // Skipping: This test fails in CI when mock is not found at expected path
54
+ const response = await client.callTool('run', {
55
+ prompt: 'Create a file with content "Hello \\"World\\""',
56
+ workFolder: testDir,
57
+ });
58
+ expect(response).toBeTruthy();
59
+ });
60
+ it('should handle prompts with newlines', async () => {
61
+ const response = await client.callTool('run', {
62
+ prompt: 'Create a file with content:\\nLine 1\\nLine 2',
63
+ workFolder: testDir,
64
+ });
65
+ expect(response).toBeTruthy();
66
+ });
67
+ it('should handle prompts with shell special characters', async () => {
68
+ const response = await client.callTool('run', {
69
+ prompt: 'Create a file named test$file.txt',
70
+ workFolder: testDir,
71
+ });
72
+ expect(response).toBeTruthy();
73
+ });
74
+ });
75
+ describe('Error Recovery', () => {
76
+ it('should handle Claude CLI not found gracefully', async () => {
77
+ // Create a client with a different binary name that doesn't exist
78
+ const errorClient = createTestClient({ claudeCliName: 'non-existent-claude' });
79
+ await errorClient.connect();
80
+ await expect(errorClient.callTool('run', {
81
+ prompt: 'Test prompt',
82
+ workFolder: testDir,
83
+ })).rejects.toThrow();
84
+ await errorClient.disconnect();
85
+ });
86
+ it('should handle permission denied errors', async () => {
87
+ const restrictedDir = '/root/restricted';
88
+ // Non-existent directories now throw an error
89
+ await expect(client.callTool('run', {
90
+ prompt: 'Test prompt',
91
+ workFolder: restrictedDir,
92
+ })).rejects.toThrow(/does not exist/i);
93
+ });
94
+ });
95
+ describe('Concurrent Requests', () => {
96
+ it('should handle multiple simultaneous requests', async () => {
97
+ const promises = Array(5).fill(null).map((_, i) => client.callTool('run', {
98
+ prompt: `Create file test${i}.txt`,
99
+ workFolder: testDir,
100
+ }));
101
+ const results = await Promise.allSettled(promises);
102
+ const successful = results.filter(r => r.status === 'fulfilled');
103
+ expect(successful.length).toBeGreaterThan(0);
104
+ });
105
+ });
106
+ describe('Large Prompts', () => {
107
+ it('should handle very long prompts', async () => {
108
+ const longPrompt = 'Create a file with content: ' + 'x'.repeat(10000);
109
+ const response = await client.callTool('run', {
110
+ prompt: longPrompt,
111
+ workFolder: testDir,
112
+ });
113
+ expect(response).toBeTruthy();
114
+ });
115
+ });
116
+ describe('Path Traversal', () => {
117
+ it('should prevent path traversal attacks', async () => {
118
+ const maliciousPath = join(testDir, '..', '..', 'etc', 'passwd');
119
+ // Server resolves paths and checks existence
120
+ // The path /etc/passwd may exist but be a file, not a directory
121
+ await expect(client.callTool('run', {
122
+ prompt: 'Read file',
123
+ workFolder: maliciousPath,
124
+ })).rejects.toThrow(/(does not exist|ENOTDIR)/i);
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,291 @@
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
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ // Mock dependencies
8
+ vi.mock('node:child_process');
9
+ vi.mock('node:fs');
10
+ vi.mock('node:os');
11
+ vi.mock('node:path', () => ({
12
+ resolve: vi.fn((path) => path),
13
+ join: vi.fn((...args) => args.join('/')),
14
+ isAbsolute: vi.fn((path) => path.startsWith('/'))
15
+ }));
16
+ vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
17
+ Server: vi.fn().mockImplementation(function () {
18
+ this.setRequestHandler = vi.fn();
19
+ this.connect = vi.fn();
20
+ this.close = vi.fn();
21
+ this.onerror = undefined;
22
+ return this;
23
+ }),
24
+ }));
25
+ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
26
+ ListToolsRequestSchema: { name: 'listTools' },
27
+ CallToolRequestSchema: { name: 'callTool' },
28
+ ErrorCode: {
29
+ InternalError: 'InternalError',
30
+ MethodNotFound: 'MethodNotFound',
31
+ InvalidParams: 'InvalidParams'
32
+ },
33
+ McpError: class extends Error {
34
+ code;
35
+ constructor(code, message) {
36
+ super(message);
37
+ this.code = code;
38
+ }
39
+ }
40
+ }));
41
+ const mockExistsSync = vi.mocked(existsSync);
42
+ const mockSpawn = vi.mocked(spawn);
43
+ const mockHomedir = vi.mocked(homedir);
44
+ describe('Error Handling Tests', () => {
45
+ let consoleErrorSpy;
46
+ let originalEnv;
47
+ let errorHandler = null;
48
+ function setupServerMock() {
49
+ errorHandler = null;
50
+ vi.mocked(Server).mockImplementation(function () {
51
+ this.setRequestHandler = vi.fn();
52
+ this.connect = vi.fn();
53
+ this.close = vi.fn();
54
+ Object.defineProperty(this, 'onerror', {
55
+ get() { return errorHandler; },
56
+ set(handler) { errorHandler = handler; },
57
+ enumerable: true,
58
+ configurable: true
59
+ });
60
+ return this;
61
+ });
62
+ }
63
+ beforeEach(() => {
64
+ vi.clearAllMocks();
65
+ vi.resetModules();
66
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
67
+ originalEnv = { ...process.env };
68
+ process.env = { ...originalEnv };
69
+ });
70
+ afterEach(() => {
71
+ consoleErrorSpy.mockRestore();
72
+ process.env = originalEnv;
73
+ });
74
+ describe('CallToolRequest Error Cases', () => {
75
+ it('should throw error for unknown tool name', async () => {
76
+ mockHomedir.mockReturnValue('/home/user');
77
+ mockExistsSync.mockReturnValue(true);
78
+ // Set up Server mock before importing the module
79
+ setupServerMock();
80
+ const module = await import('../server.js');
81
+ // @ts-ignore
82
+ const { ClaudeCodeServer } = module;
83
+ const server = new ClaudeCodeServer();
84
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
85
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
86
+ const handler = callToolCall[1];
87
+ await expect(handler({
88
+ params: {
89
+ name: 'unknown_tool',
90
+ arguments: {}
91
+ }
92
+ })).rejects.toThrow('Tool unknown_tool not found');
93
+ });
94
+ it('should handle timeout errors', async () => {
95
+ mockHomedir.mockReturnValue('/home/user');
96
+ mockExistsSync.mockReturnValue(true);
97
+ setupServerMock();
98
+ const module = await import('../server.js');
99
+ // @ts-ignore
100
+ const { ClaudeCodeServer } = module;
101
+ const { McpError } = await import('@modelcontextprotocol/sdk/types.js');
102
+ const server = new ClaudeCodeServer();
103
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
104
+ // Find the callTool handler
105
+ let callToolHandler;
106
+ for (const call of mockServerInstance.setRequestHandler.mock.calls) {
107
+ if (call[0].name === 'callTool') {
108
+ callToolHandler = call[1];
109
+ break;
110
+ }
111
+ }
112
+ // Mock spawn to return process without PID
113
+ mockSpawn.mockImplementation(() => {
114
+ const mockProcess = new EventEmitter();
115
+ mockProcess.stdout = new EventEmitter();
116
+ mockProcess.stderr = new EventEmitter();
117
+ mockProcess.stdout.on = vi.fn();
118
+ mockProcess.stderr.on = vi.fn();
119
+ mockProcess.pid = undefined; // No PID to simulate process start failure
120
+ return mockProcess;
121
+ });
122
+ // Call handler
123
+ await expect(callToolHandler({
124
+ params: {
125
+ name: 'run',
126
+ arguments: {
127
+ prompt: 'test',
128
+ workFolder: '/tmp'
129
+ }
130
+ }
131
+ })).rejects.toThrow('Failed to start claude CLI process');
132
+ });
133
+ it('should handle invalid argument types', async () => {
134
+ mockHomedir.mockReturnValue('/home/user');
135
+ mockExistsSync.mockReturnValue(true);
136
+ setupServerMock();
137
+ const module = await import('../server.js');
138
+ // @ts-ignore
139
+ const { ClaudeCodeServer } = module;
140
+ const server = new ClaudeCodeServer();
141
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
142
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
143
+ const handler = callToolCall[1];
144
+ await expect(handler({
145
+ params: {
146
+ name: 'run',
147
+ arguments: 'invalid-should-be-object'
148
+ }
149
+ })).rejects.toThrow();
150
+ });
151
+ it('should include CLI error details in error message', async () => {
152
+ mockHomedir.mockReturnValue('/home/user');
153
+ mockExistsSync.mockReturnValue(true);
154
+ setupServerMock();
155
+ const module = await import('../server.js');
156
+ // @ts-ignore
157
+ const { ClaudeCodeServer } = module;
158
+ const server = new ClaudeCodeServer();
159
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
160
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
161
+ const handler = callToolCall[1];
162
+ // Create a simple mock process
163
+ mockSpawn.mockImplementation(() => {
164
+ const mockProcess = Object.create(EventEmitter.prototype);
165
+ EventEmitter.call(mockProcess);
166
+ mockProcess.stdout = Object.create(EventEmitter.prototype);
167
+ EventEmitter.call(mockProcess.stdout);
168
+ mockProcess.stderr = Object.create(EventEmitter.prototype);
169
+ EventEmitter.call(mockProcess.stderr);
170
+ mockProcess.stdout.on = vi.fn((event, callback) => {
171
+ if (event === 'data') {
172
+ // Send some stdout data
173
+ process.nextTick(() => callback('stdout content'));
174
+ }
175
+ });
176
+ mockProcess.stderr.on = vi.fn((event, callback) => {
177
+ if (event === 'data') {
178
+ // Send some stderr data
179
+ process.nextTick(() => callback('stderr content'));
180
+ }
181
+ });
182
+ // Emit error/close event after data is sent
183
+ setTimeout(() => {
184
+ mockProcess.emit('close', 1);
185
+ }, 1);
186
+ return mockProcess;
187
+ });
188
+ await expect(handler({
189
+ params: {
190
+ name: 'run',
191
+ arguments: {
192
+ prompt: 'test',
193
+ workFolder: '/tmp'
194
+ }
195
+ }
196
+ })).rejects.toThrow();
197
+ });
198
+ });
199
+ describe('Process Spawn Error Cases', () => {
200
+ it('should handle spawn ENOENT error', async () => {
201
+ const module = await import('../server.js');
202
+ // @ts-ignore
203
+ const { spawnAsync } = module;
204
+ const mockProcess = new EventEmitter();
205
+ mockProcess.stdout = new EventEmitter();
206
+ mockProcess.stderr = new EventEmitter();
207
+ mockProcess.stdout.on = vi.fn();
208
+ mockProcess.stderr.on = vi.fn();
209
+ mockSpawn.mockReturnValue(mockProcess);
210
+ const promise = spawnAsync('nonexistent-command', []);
211
+ // Simulate ENOENT error
212
+ setTimeout(() => {
213
+ const error = new Error('spawn ENOENT');
214
+ error.code = 'ENOENT';
215
+ error.path = 'nonexistent-command';
216
+ error.syscall = 'spawn';
217
+ mockProcess.emit('error', error);
218
+ }, 10);
219
+ await expect(promise).rejects.toThrow('Spawn error');
220
+ await expect(promise).rejects.toThrow('nonexistent-command');
221
+ });
222
+ it('should handle generic spawn errors', async () => {
223
+ const module = await import('../server.js');
224
+ // @ts-ignore
225
+ const { spawnAsync } = module;
226
+ const mockProcess = new EventEmitter();
227
+ mockProcess.stdout = new EventEmitter();
228
+ mockProcess.stderr = new EventEmitter();
229
+ mockProcess.stdout.on = vi.fn();
230
+ mockProcess.stderr.on = vi.fn();
231
+ mockSpawn.mockReturnValue(mockProcess);
232
+ const promise = spawnAsync('test', []);
233
+ // Simulate generic error
234
+ setTimeout(() => {
235
+ mockProcess.emit('error', new Error('Generic spawn error'));
236
+ }, 10);
237
+ await expect(promise).rejects.toThrow('Generic spawn error');
238
+ });
239
+ it('should accumulate stderr output before error', async () => {
240
+ const module = await import('../server.js');
241
+ // @ts-ignore
242
+ const { spawnAsync } = module;
243
+ const mockProcess = new EventEmitter();
244
+ mockProcess.stdout = new EventEmitter();
245
+ mockProcess.stderr = new EventEmitter();
246
+ let stderrHandler;
247
+ mockProcess.stdout.on = vi.fn();
248
+ mockProcess.stderr.on = vi.fn((event, handler) => {
249
+ if (event === 'data')
250
+ stderrHandler = handler;
251
+ });
252
+ mockSpawn.mockReturnValue(mockProcess);
253
+ const promise = spawnAsync('test', []);
254
+ // Simulate stderr data then error
255
+ setTimeout(() => {
256
+ stderrHandler('error line 1\n');
257
+ stderrHandler('error line 2\n');
258
+ mockProcess.emit('error', new Error('Command failed'));
259
+ }, 10);
260
+ await expect(promise).rejects.toThrow('error line 1\nerror line 2');
261
+ });
262
+ });
263
+ describe('Server Initialization Errors', () => {
264
+ it('should handle CLI path not found gracefully', async () => {
265
+ // Mock no CLI found anywhere
266
+ mockHomedir.mockReturnValue('/home/user');
267
+ mockExistsSync.mockReturnValue(false);
268
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
269
+ setupServerMock();
270
+ const module = await import('../server.js');
271
+ // @ts-ignore
272
+ const { ClaudeCodeServer } = module;
273
+ const server = new ClaudeCodeServer();
274
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Claude CLI not found'));
275
+ consoleWarnSpy.mockRestore();
276
+ });
277
+ it('should handle server connection errors', async () => {
278
+ mockHomedir.mockReturnValue('/home/user');
279
+ mockExistsSync.mockReturnValue(true);
280
+ setupServerMock();
281
+ const module = await import('../server.js');
282
+ // @ts-ignore
283
+ const { ClaudeCodeServer } = module;
284
+ const server = new ClaudeCodeServer();
285
+ // Mock connection failure
286
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
287
+ mockServerInstance.connect.mockRejectedValue(new Error('Connection failed'));
288
+ await expect(server.run()).rejects.toThrow('Connection failed');
289
+ });
290
+ });
291
+ });
@@ -0,0 +1,32 @@
1
+ import { vi } from 'vitest';
2
+ // Mock Claude CLI responses
3
+ export const mockClaudeResponse = (stdout, stderr = '', exitCode = 0) => {
4
+ return {
5
+ stdout: { on: vi.fn((event, cb) => event === 'data' && cb(stdout)) },
6
+ stderr: { on: vi.fn((event, cb) => event === 'data' && cb(stderr)) },
7
+ on: vi.fn((event, cb) => {
8
+ if (event === 'exit')
9
+ setTimeout(() => cb(exitCode), 10);
10
+ }),
11
+ };
12
+ };
13
+ // Mock MCP request builder
14
+ export const createMCPRequest = (tool, args, id = 1) => ({
15
+ jsonrpc: '2.0',
16
+ method: 'tools/call',
17
+ params: {
18
+ name: tool,
19
+ arguments: args,
20
+ },
21
+ id,
22
+ });
23
+ // Mock file system operations
24
+ export const setupTestEnvironment = () => {
25
+ const testFiles = new Map();
26
+ return {
27
+ writeFile: (path, content) => testFiles.set(path, content),
28
+ readFile: (path) => testFiles.get(path),
29
+ exists: (path) => testFiles.has(path),
30
+ cleanup: () => testFiles.clear(),
31
+ };
32
+ };