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,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
|
+
});
|