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