ai-cli-mcp 2.19.0 → 2.20.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 (100) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +34 -8
  3. package/README.md +41 -8
  4. package/dist/app/cli.js +1 -0
  5. package/dist/app/mcp.js +64 -12
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +76 -91
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +8 -2
  12. package/package.json +27 -3
  13. package/server.json +3 -3
  14. package/.gemini/settings.json +0 -11
  15. package/.github/dependabot.yml +0 -28
  16. package/.github/pull_request_template.md +0 -28
  17. package/.github/workflows/ci.yml +0 -34
  18. package/.github/workflows/dependency-review.yml +0 -22
  19. package/.github/workflows/publish.yml +0 -89
  20. package/.github/workflows/test.yml +0 -20
  21. package/.github/workflows/watch-session-prs.yml +0 -276
  22. package/.husky/pre-commit +0 -1
  23. package/.mcp.json +0 -11
  24. package/.releaserc.json +0 -18
  25. package/.vscode/settings.json +0 -3
  26. package/CONTRIBUTING.md +0 -81
  27. package/dist/__tests__/app-cli.test.js +0 -392
  28. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  29. package/dist/__tests__/cli-builder.test.js +0 -442
  30. package/dist/__tests__/cli-process-service.test.js +0 -655
  31. package/dist/__tests__/cli-utils.test.js +0 -171
  32. package/dist/__tests__/e2e.test.js +0 -256
  33. package/dist/__tests__/edge-cases.test.js +0 -130
  34. package/dist/__tests__/error-cases.test.js +0 -292
  35. package/dist/__tests__/mcp-contract.test.js +0 -636
  36. package/dist/__tests__/mocks.js +0 -32
  37. package/dist/__tests__/model-alias.test.js +0 -36
  38. package/dist/__tests__/parsers.test.js +0 -646
  39. package/dist/__tests__/peek.test.js +0 -36
  40. package/dist/__tests__/process-management.test.js +0 -949
  41. package/dist/__tests__/server.test.js +0 -809
  42. package/dist/__tests__/setup.js +0 -11
  43. package/dist/__tests__/utils/claude-mock.js +0 -80
  44. package/dist/__tests__/utils/mcp-client.js +0 -121
  45. package/dist/__tests__/utils/opencode-mock.js +0 -91
  46. package/dist/__tests__/utils/persistent-mock.js +0 -28
  47. package/dist/__tests__/utils/test-helpers.js +0 -11
  48. package/dist/__tests__/validation.test.js +0 -308
  49. package/dist/__tests__/version-print.test.js +0 -65
  50. package/dist/__tests__/wait.test.js +0 -260
  51. package/docs/RELEASE_CHECKLIST.md +0 -65
  52. package/docs/cli-architecture.md +0 -275
  53. package/docs/concept.md +0 -154
  54. package/docs/development.md +0 -156
  55. package/docs/e2e-testing.md +0 -148
  56. package/docs/prd.md +0 -146
  57. package/docs/session-stacking.md +0 -67
  58. package/src/__tests__/app-cli.test.ts +0 -495
  59. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  60. package/src/__tests__/cli-builder.test.ts +0 -549
  61. package/src/__tests__/cli-process-service.test.ts +0 -759
  62. package/src/__tests__/cli-utils.test.ts +0 -200
  63. package/src/__tests__/e2e.test.ts +0 -311
  64. package/src/__tests__/edge-cases.test.ts +0 -176
  65. package/src/__tests__/error-cases.test.ts +0 -370
  66. package/src/__tests__/mcp-contract.test.ts +0 -755
  67. package/src/__tests__/mocks.ts +0 -35
  68. package/src/__tests__/model-alias.test.ts +0 -44
  69. package/src/__tests__/parsers.test.ts +0 -730
  70. package/src/__tests__/peek.test.ts +0 -44
  71. package/src/__tests__/process-management.test.ts +0 -1129
  72. package/src/__tests__/server.test.ts +0 -1020
  73. package/src/__tests__/setup.ts +0 -13
  74. package/src/__tests__/utils/claude-mock.ts +0 -87
  75. package/src/__tests__/utils/mcp-client.ts +0 -159
  76. package/src/__tests__/utils/opencode-mock.ts +0 -108
  77. package/src/__tests__/utils/persistent-mock.ts +0 -33
  78. package/src/__tests__/utils/test-helpers.ts +0 -13
  79. package/src/__tests__/validation.test.ts +0 -369
  80. package/src/__tests__/version-print.test.ts +0 -81
  81. package/src/__tests__/wait.test.ts +0 -302
  82. package/src/app/cli.ts +0 -424
  83. package/src/app/mcp.ts +0 -466
  84. package/src/bin/ai-cli-mcp.ts +0 -7
  85. package/src/bin/ai-cli.ts +0 -11
  86. package/src/cli-builder.ts +0 -274
  87. package/src/cli-parse.ts +0 -105
  88. package/src/cli-process-service.ts +0 -709
  89. package/src/cli-utils.ts +0 -258
  90. package/src/cli.ts +0 -124
  91. package/src/model-catalog.ts +0 -87
  92. package/src/parsers.ts +0 -965
  93. package/src/peek.ts +0 -95
  94. package/src/process-result.ts +0 -88
  95. package/src/process-service.ts +0 -368
  96. package/src/server.ts +0 -10
  97. package/tsconfig.json +0 -16
  98. package/vitest.config.e2e.ts +0 -27
  99. package/vitest.config.ts +0 -22
  100. package/vitest.config.unit.ts +0 -28
@@ -1,636 +0,0 @@
1
- import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
2
- import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { cleanupSharedMock, getSharedMock } from './utils/persistent-mock.js';
6
- import { createOpenCodeMock } from './utils/opencode-mock.js';
7
- import { createTestClient } from './utils/mcp-client.js';
8
- function parseToolJson(content) {
9
- expect(content).toHaveLength(1);
10
- expect(content[0].type).toBe('text');
11
- return JSON.parse(content[0].text);
12
- }
13
- function expectProcessSummaryShape(processInfo) {
14
- expect(processInfo).toEqual({
15
- pid: expect.any(Number),
16
- agent: expect.any(String),
17
- status: expect.any(String),
18
- });
19
- }
20
- function createForgeMockScript(dir, argsLogPath) {
21
- const scriptPath = join(dir, 'mock-forge');
22
- writeFileSync(scriptPath, `#!/bin/bash
23
- set -euo pipefail
24
-
25
- log_file="${argsLogPath}"
26
- prompt=""
27
- conversation_id=""
28
-
29
- printf '%s\\n' "$*" >> "$log_file"
30
-
31
- while [[ $# -gt 0 ]]; do
32
- case "$1" in
33
- -C)
34
- shift 2
35
- ;;
36
- -p)
37
- prompt="$2"
38
- shift 2
39
- ;;
40
- --conversation-id)
41
- conversation_id="$2"
42
- shift 2
43
- ;;
44
- *)
45
- shift
46
- ;;
47
- esac
48
- done
49
-
50
- if [[ -n "$conversation_id" ]]; then
51
- printf '● [21:09:33] Continue %s\\n' "$conversation_id"
52
- printf 'Resumed: %s\\n' "$prompt"
53
- printf '● [21:09:37] Finished %s\\n' "$conversation_id"
54
- else
55
- printf '● [21:09:01] Initialize forge-session-1\\n'
56
- printf 'Initial: %s\\n' "$prompt"
57
- printf '● [21:09:08] Finished forge-session-1\\n'
58
- fi
59
- `);
60
- chmodSync(scriptPath, 0o755);
61
- return scriptPath;
62
- }
63
- describe('MCP Contract Tests', () => {
64
- let client;
65
- let testDir;
66
- beforeEach(async () => {
67
- await getSharedMock();
68
- testDir = mkdtempSync(join(tmpdir(), 'ai-cli-mcp-contract-'));
69
- client = createTestClient({ debug: false });
70
- await client.connect();
71
- });
72
- afterEach(async () => {
73
- await client.disconnect();
74
- rmSync(testDir, { recursive: true, force: true });
75
- });
76
- afterAll(async () => {
77
- await cleanupSharedMock();
78
- });
79
- it('registers the current MCP tool contract', async () => {
80
- const tools = await client.listTools();
81
- const toolNames = tools.map((tool) => tool.name).sort();
82
- expect(toolNames).toEqual([
83
- 'cleanup_processes',
84
- 'get_result',
85
- 'kill_process',
86
- 'list_processes',
87
- 'peek',
88
- 'run',
89
- 'wait',
90
- ]);
91
- const runTool = tools.find((tool) => tool.name === 'run');
92
- expect(runTool.inputSchema.required).toEqual(['workFolder']);
93
- expect(Object.keys(runTool.inputSchema.properties).sort()).toEqual([
94
- 'model',
95
- 'prompt',
96
- 'prompt_file',
97
- 'reasoning_effort',
98
- 'session_id',
99
- 'workFolder',
100
- ]);
101
- expect(runTool.description).toContain('OpenCode');
102
- expect(runTool.inputSchema.properties.model.description).toContain('opencode');
103
- expect(runTool.inputSchema.properties.model.description).toContain('oc-<provider/model>');
104
- expect(runTool.inputSchema.properties.reasoning_effort.description).toContain('OpenCode do not support reasoning_effort');
105
- expect(runTool.inputSchema.properties.session_id.description).toBe('Optional session ID to resume a previous session. Supported for Claude, Codex, Gemini, Forge, and OpenCode. OpenCode resumes in-place via --session and may also be combined with explicit oc-<provider/model> selection.');
106
- const getResultTool = tools.find((tool) => tool.name === 'get_result');
107
- expect(getResultTool.inputSchema.required).toEqual(['pid']);
108
- expect(Object.keys(getResultTool.inputSchema.properties).sort()).toEqual([
109
- 'pid',
110
- 'verbose',
111
- ]);
112
- const waitTool = tools.find((tool) => tool.name === 'wait');
113
- expect(waitTool.inputSchema.required).toEqual(['pids']);
114
- expect(Object.keys(waitTool.inputSchema.properties).sort()).toEqual([
115
- 'pids',
116
- 'timeout',
117
- 'verbose',
118
- ]);
119
- const peekTool = tools.find((tool) => tool.name === 'peek');
120
- expect(peekTool.inputSchema.required).toEqual(['pids']);
121
- expect(Object.keys(peekTool.inputSchema.properties).sort()).toEqual([
122
- 'include_tool_calls',
123
- 'peek_time_sec',
124
- 'pids',
125
- ]);
126
- expect(peekTool.description).toContain('One-shot');
127
- });
128
- it('preserves the stdio MCP smoke flow and response shapes', async () => {
129
- const runResponse = await client.callTool('run', {
130
- prompt: 'create a file called contract.txt with content "hello"',
131
- workFolder: testDir,
132
- model: 'haiku',
133
- });
134
- const runData = parseToolJson(runResponse);
135
- expect(runData).toEqual({
136
- pid: expect.any(Number),
137
- status: 'started',
138
- agent: 'claude',
139
- message: expect.any(String),
140
- });
141
- const listResponse = await client.callTool('list_processes', {});
142
- const listData = parseToolJson(listResponse);
143
- const listedRun = listData.find((entry) => entry.pid === runData.pid);
144
- expect(Array.isArray(listData)).toBe(true);
145
- expect(listedRun).toBeTruthy();
146
- expectProcessSummaryShape(listedRun);
147
- const getResultResponse = await client.callTool('get_result', { pid: runData.pid });
148
- const getResultData = parseToolJson(getResultResponse);
149
- expect(getResultData).toMatchObject({
150
- pid: runData.pid,
151
- agent: 'claude',
152
- status: expect.any(String),
153
- model: 'haiku',
154
- stdout: expect.any(String),
155
- stderr: expect.any(String),
156
- });
157
- expect(getResultData).toHaveProperty('exitCode');
158
- expect(getResultData).not.toHaveProperty('startTime');
159
- expect(getResultData).not.toHaveProperty('workFolder');
160
- expect(getResultData).not.toHaveProperty('prompt');
161
- const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
162
- const waitData = parseToolJson(waitResponse);
163
- expect(Array.isArray(waitData)).toBe(true);
164
- expect(waitData).toHaveLength(1);
165
- expect(waitData[0]).toMatchObject({
166
- pid: runData.pid,
167
- agent: 'claude',
168
- status: 'completed',
169
- exitCode: 0,
170
- model: 'haiku',
171
- stdout: expect.any(String),
172
- stderr: expect.any(String),
173
- });
174
- expect(waitData[0]).not.toHaveProperty('startTime');
175
- expect(waitData[0]).not.toHaveProperty('workFolder');
176
- expect(waitData[0]).not.toHaveProperty('prompt');
177
- const cleanupResponse = await client.callTool('cleanup_processes', {});
178
- const cleanupData = parseToolJson(cleanupResponse);
179
- expect(cleanupData).toEqual({
180
- removed: expect.any(Number),
181
- removedPids: expect.any(Array),
182
- message: expect.any(String),
183
- });
184
- expect(cleanupData.removedPids).toContain(runData.pid);
185
- });
186
- it('preserves successful prompt_file execution through the MCP process path', async () => {
187
- const promptFile = join(testDir, 'prompt.txt');
188
- writeFileSync(promptFile, 'Create a file from prompt_file');
189
- const runResponse = await client.callTool('run', {
190
- prompt_file: promptFile,
191
- workFolder: testDir,
192
- model: 'haiku',
193
- });
194
- const runData = parseToolJson(runResponse);
195
- expect(runData).toEqual({
196
- pid: expect.any(Number),
197
- status: 'started',
198
- agent: 'claude',
199
- message: expect.any(String),
200
- });
201
- const waitResponse = await client.callTool('wait', { pids: [runData.pid], timeout: 5 });
202
- const waitData = parseToolJson(waitResponse);
203
- expect(waitData).toHaveLength(1);
204
- expect(waitData[0]).toMatchObject({
205
- pid: runData.pid,
206
- agent: 'claude',
207
- status: 'completed',
208
- exitCode: 0,
209
- model: 'haiku',
210
- stdout: expect.stringContaining('Created file successfully'),
211
- stderr: '',
212
- });
213
- expect(waitData[0]).not.toHaveProperty('prompt');
214
- expect(waitData[0]).not.toHaveProperty('workFolder');
215
- expect(waitData[0]).not.toHaveProperty('startTime');
216
- });
217
- it('returns compact results by default and full results when verbose is true for parsed output', async () => {
218
- await client.disconnect();
219
- const verboseMockPath = join(testDir, 'verbose-claude');
220
- writeFileSync(verboseMockPath, `#!/bin/bash
221
- printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
222
- printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
223
- printf '%s\n' '{"type":"result","result":"Completed contract verbose test"}'
224
- printf '%s\n' '{"type":"system","session_id":"session-verbose-1"}'
225
- `);
226
- chmodSync(verboseMockPath, 0o755);
227
- client = createTestClient({ claudeCliName: verboseMockPath, debug: false });
228
- await client.connect();
229
- const runResponse = await client.callTool('run', {
230
- prompt: 'verbose-shape-test',
231
- workFolder: testDir,
232
- });
233
- const runData = parseToolJson(runResponse);
234
- const completedWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
235
- expect(completedWait).toHaveLength(1);
236
- expect(completedWait[0].status).toBe('completed');
237
- const compactResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid }));
238
- expect(compactResult).toMatchObject({
239
- pid: runData.pid,
240
- agent: 'claude',
241
- status: 'completed',
242
- exitCode: 0,
243
- model: null,
244
- session_id: 'session-verbose-1',
245
- agentOutput: {
246
- message: 'Completed contract verbose test',
247
- session_id: 'session-verbose-1',
248
- },
249
- });
250
- expect(compactResult).not.toHaveProperty('startTime');
251
- expect(compactResult).not.toHaveProperty('workFolder');
252
- expect(compactResult).not.toHaveProperty('prompt');
253
- expect(compactResult.agentOutput).not.toHaveProperty('tools');
254
- const verboseResult = parseToolJson(await client.callTool('get_result', { pid: runData.pid, verbose: true }));
255
- expect(verboseResult).toMatchObject({
256
- pid: runData.pid,
257
- agent: 'claude',
258
- status: 'completed',
259
- exitCode: 0,
260
- model: null,
261
- startTime: expect.any(String),
262
- workFolder: testDir,
263
- prompt: 'verbose-shape-test',
264
- session_id: 'session-verbose-1',
265
- agentOutput: {
266
- message: 'Completed contract verbose test',
267
- session_id: 'session-verbose-1',
268
- tools: [
269
- {
270
- tool: 'Read',
271
- input: { file_path: '/tmp/demo.txt' },
272
- output: 'demo output',
273
- },
274
- ],
275
- },
276
- });
277
- const compactWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5 }));
278
- expect(compactWait).toHaveLength(1);
279
- expect(compactWait[0]).toMatchObject({
280
- pid: runData.pid,
281
- agent: 'claude',
282
- status: 'completed',
283
- exitCode: 0,
284
- model: null,
285
- session_id: 'session-verbose-1',
286
- agentOutput: {
287
- message: 'Completed contract verbose test',
288
- session_id: 'session-verbose-1',
289
- },
290
- });
291
- expect(compactWait[0]).not.toHaveProperty('startTime');
292
- expect(compactWait[0]).not.toHaveProperty('workFolder');
293
- expect(compactWait[0]).not.toHaveProperty('prompt');
294
- expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
295
- const verboseWait = parseToolJson(await client.callTool('wait', { pids: [runData.pid], timeout: 5, verbose: true }));
296
- expect(verboseWait).toHaveLength(1);
297
- expect(verboseWait[0]).toMatchObject({
298
- pid: runData.pid,
299
- agent: 'claude',
300
- status: 'completed',
301
- exitCode: 0,
302
- model: null,
303
- startTime: expect.any(String),
304
- workFolder: testDir,
305
- prompt: 'verbose-shape-test',
306
- session_id: 'session-verbose-1',
307
- agentOutput: {
308
- message: 'Completed contract verbose test',
309
- session_id: 'session-verbose-1',
310
- tools: [
311
- {
312
- tool: 'Read',
313
- input: { file_path: '/tmp/demo.txt' },
314
- output: 'demo output',
315
- },
316
- ],
317
- },
318
- });
319
- });
320
- it('covers forge end-to-end through the MCP process path', async () => {
321
- await client.disconnect();
322
- const forgeArgsLogPath = join(testDir, 'forge-args.log');
323
- const forgeMockPath = createForgeMockScript(testDir, forgeArgsLogPath);
324
- client = createTestClient({
325
- debug: false,
326
- env: {
327
- FORGE_CLI_NAME: forgeMockPath,
328
- },
329
- });
330
- await client.connect();
331
- const initialRunResponse = await client.callTool('run', {
332
- prompt: 'forge-initial-prompt',
333
- workFolder: testDir,
334
- model: 'forge',
335
- });
336
- const initialRunData = parseToolJson(initialRunResponse);
337
- expect(initialRunData).toEqual({
338
- pid: expect.any(Number),
339
- status: 'started',
340
- agent: 'forge',
341
- message: expect.any(String),
342
- });
343
- const initialWaitResponse = await client.callTool('wait', { pids: [initialRunData.pid], timeout: 5 });
344
- const initialWaitData = parseToolJson(initialWaitResponse);
345
- expect(initialWaitData).toHaveLength(1);
346
- expect(initialWaitData[0]).toMatchObject({
347
- pid: initialRunData.pid,
348
- agent: 'forge',
349
- status: 'completed',
350
- session_id: 'forge-session-1',
351
- agentOutput: {
352
- message: 'Initial: forge-initial-prompt',
353
- session_id: 'forge-session-1',
354
- },
355
- });
356
- const initialResultResponse = await client.callTool('get_result', { pid: initialRunData.pid });
357
- const initialResultData = parseToolJson(initialResultResponse);
358
- expect(initialResultData).toMatchObject({
359
- pid: initialRunData.pid,
360
- agent: 'forge',
361
- status: 'completed',
362
- session_id: 'forge-session-1',
363
- agentOutput: {
364
- message: 'Initial: forge-initial-prompt',
365
- session_id: 'forge-session-1',
366
- },
367
- });
368
- const resumedRunResponse = await client.callTool('run', {
369
- prompt: 'forge-resume-prompt',
370
- workFolder: testDir,
371
- model: 'forge',
372
- session_id: 'forge-session-1',
373
- });
374
- const resumedRunData = parseToolJson(resumedRunResponse);
375
- expect(resumedRunData).toEqual({
376
- pid: expect.any(Number),
377
- status: 'started',
378
- agent: 'forge',
379
- message: expect.any(String),
380
- });
381
- const resumedWaitResponse = await client.callTool('wait', { pids: [resumedRunData.pid], timeout: 5 });
382
- const resumedWaitData = parseToolJson(resumedWaitResponse);
383
- expect(resumedWaitData).toHaveLength(1);
384
- expect(resumedWaitData[0]).toMatchObject({
385
- pid: resumedRunData.pid,
386
- agent: 'forge',
387
- status: 'completed',
388
- session_id: 'forge-session-1',
389
- agentOutput: {
390
- message: 'Resumed: forge-resume-prompt',
391
- session_id: 'forge-session-1',
392
- },
393
- });
394
- const resumedResultResponse = await client.callTool('get_result', { pid: resumedRunData.pid });
395
- const resumedResultData = parseToolJson(resumedResultResponse);
396
- expect(resumedResultData).toMatchObject({
397
- pid: resumedRunData.pid,
398
- agent: 'forge',
399
- status: 'completed',
400
- session_id: 'forge-session-1',
401
- agentOutput: {
402
- message: 'Resumed: forge-resume-prompt',
403
- session_id: 'forge-session-1',
404
- },
405
- });
406
- const forgeInvocations = readFileSync(forgeArgsLogPath, 'utf-8').trim().split('\n');
407
- expect(forgeInvocations).toHaveLength(2);
408
- expect(forgeInvocations[0]).toContain(`-C ${testDir}`);
409
- expect(forgeInvocations[0]).toContain('-p forge-initial-prompt');
410
- expect(forgeInvocations[0]).not.toContain('--model');
411
- expect(forgeInvocations[0]).not.toContain('--agent');
412
- expect(forgeInvocations[0]).not.toContain('--conversation-id');
413
- expect(forgeInvocations[1]).toContain(`-C ${testDir}`);
414
- expect(forgeInvocations[1]).toContain('--conversation-id forge-session-1');
415
- expect(forgeInvocations[1]).toContain('-p forge-resume-prompt');
416
- expect(forgeInvocations[1]).not.toContain('--model');
417
- expect(forgeInvocations[1]).not.toContain('--agent');
418
- await expect(client.callTool('run', {
419
- prompt: 'forge-invalid-reasoning',
420
- workFolder: testDir,
421
- model: 'forge',
422
- reasoning_effort: 'high',
423
- })).rejects.toThrow(/reasoning_effort is not supported for forge/i);
424
- });
425
- it('covers OpenCode end-to-end through the MCP process path', async () => {
426
- await client.disconnect();
427
- const opencodeArgsLogPath = join(testDir, 'opencode-args.log');
428
- const { scriptPath: openCodeMockPath } = createOpenCodeMock(testDir, {
429
- argsLogPath: opencodeArgsLogPath,
430
- defaultSessionId: 'ses-opencode-contract',
431
- });
432
- client = createTestClient({
433
- debug: false,
434
- env: {
435
- OPENCODE_CLI_NAME: openCodeMockPath,
436
- },
437
- });
438
- await client.connect();
439
- const initialRunResponse = await client.callTool('run', {
440
- prompt: 'opencode-initial-prompt',
441
- workFolder: testDir,
442
- model: 'opencode',
443
- });
444
- const initialRunData = parseToolJson(initialRunResponse);
445
- expect(initialRunData).toEqual({
446
- pid: expect.any(Number),
447
- status: 'started',
448
- agent: 'opencode',
449
- message: expect.any(String),
450
- });
451
- const initialWaitResponse = await client.callTool('wait', { pids: [initialRunData.pid], timeout: 5 });
452
- const initialWaitData = parseToolJson(initialWaitResponse);
453
- expect(initialWaitData).toHaveLength(1);
454
- expect(initialWaitData[0]).toMatchObject({
455
- pid: initialRunData.pid,
456
- agent: 'opencode',
457
- status: 'completed',
458
- exitCode: 0,
459
- model: 'opencode',
460
- session_id: 'ses-opencode-contract',
461
- agentOutput: {
462
- message: 'Initial: opencode-initial-prompt',
463
- session_id: 'ses-opencode-contract',
464
- tokens: { total: 11833 },
465
- cost: 0,
466
- },
467
- });
468
- const resumedDefaultRunResponse = await client.callTool('run', {
469
- prompt: 'opencode-resume-default',
470
- workFolder: testDir,
471
- model: 'opencode',
472
- session_id: 'ses-opencode-contract',
473
- });
474
- const resumedDefaultRunData = parseToolJson(resumedDefaultRunResponse);
475
- const resumedDefaultWaitResponse = await client.callTool('wait', { pids: [resumedDefaultRunData.pid], timeout: 5 });
476
- const resumedDefaultWaitData = parseToolJson(resumedDefaultWaitResponse);
477
- expect(resumedDefaultWaitData).toHaveLength(1);
478
- expect(resumedDefaultWaitData[0]).toMatchObject({
479
- pid: resumedDefaultRunData.pid,
480
- agent: 'opencode',
481
- status: 'completed',
482
- exitCode: 0,
483
- model: 'opencode',
484
- session_id: 'ses-opencode-contract',
485
- agentOutput: {
486
- message: 'Resumed: opencode-resume-default',
487
- session_id: 'ses-opencode-contract',
488
- tokens: { total: 11833 },
489
- cost: 0,
490
- },
491
- });
492
- const resumedExplicitRunResponse = await client.callTool('run', {
493
- prompt: 'opencode-resume-explicit',
494
- workFolder: testDir,
495
- model: 'oc-openai/gpt-5.4',
496
- session_id: 'ses-opencode-contract',
497
- });
498
- const resumedExplicitRunData = parseToolJson(resumedExplicitRunResponse);
499
- const resumedExplicitWaitResponse = await client.callTool('wait', { pids: [resumedExplicitRunData.pid], timeout: 5 });
500
- const resumedExplicitWaitData = parseToolJson(resumedExplicitWaitResponse);
501
- expect(resumedExplicitWaitData).toHaveLength(1);
502
- expect(resumedExplicitWaitData[0]).toMatchObject({
503
- pid: resumedExplicitRunData.pid,
504
- agent: 'opencode',
505
- status: 'completed',
506
- exitCode: 0,
507
- model: 'oc-openai/gpt-5.4',
508
- session_id: 'ses-opencode-contract',
509
- agentOutput: {
510
- message: 'Resumed model openai/gpt-5.4: opencode-resume-explicit',
511
- session_id: 'ses-opencode-contract',
512
- tokens: { total: 11833 },
513
- cost: 0,
514
- },
515
- });
516
- const failedRunResponse = await client.callTool('run', {
517
- prompt: 'please fail',
518
- workFolder: testDir,
519
- model: 'oc-openai/gpt-5.4',
520
- });
521
- const failedRunData = parseToolJson(failedRunResponse);
522
- const compactFailedWait = parseToolJson(await client.callTool('wait', { pids: [failedRunData.pid], timeout: 5 }));
523
- expect(compactFailedWait).toHaveLength(1);
524
- expect(compactFailedWait[0]).toMatchObject({
525
- pid: failedRunData.pid,
526
- agent: 'opencode',
527
- status: 'failed',
528
- exitCode: 7,
529
- model: 'oc-openai/gpt-5.4',
530
- session_id: 'ses-opencode-contract',
531
- stdout: expect.stringContaining('Partial failure output'),
532
- stderr: expect.stringContaining('OpenCode failed for openai/gpt-5.4'),
533
- });
534
- expect(compactFailedWait[0]).not.toHaveProperty('agentOutput');
535
- const verboseFailedResult = parseToolJson(await client.callTool('get_result', { pid: failedRunData.pid, verbose: true }));
536
- expect(verboseFailedResult).toMatchObject({
537
- pid: failedRunData.pid,
538
- agent: 'opencode',
539
- status: 'failed',
540
- exitCode: 7,
541
- model: 'oc-openai/gpt-5.4',
542
- session_id: 'ses-opencode-contract',
543
- stdout: expect.stringContaining('Partial failure output'),
544
- stderr: expect.stringContaining('OpenCode failed for openai/gpt-5.4'),
545
- agentOutput: {
546
- message: 'Partial failure output',
547
- session_id: 'ses-opencode-contract',
548
- tokens: { total: 42 },
549
- cost: 0,
550
- },
551
- });
552
- const openCodeInvocations = readFileSync(opencodeArgsLogPath, 'utf-8').trim().split('\n');
553
- expect(openCodeInvocations).toHaveLength(4);
554
- expect(openCodeInvocations[0]).toContain('run --format json');
555
- expect(openCodeInvocations[0]).toContain(`--dir ${testDir}`);
556
- expect(openCodeInvocations[0]).not.toContain('--session');
557
- expect(openCodeInvocations[0]).not.toContain('--model');
558
- expect(openCodeInvocations[1]).toContain(`--dir ${testDir}`);
559
- expect(openCodeInvocations[1]).toContain('--session ses-opencode-contract');
560
- expect(openCodeInvocations[1]).not.toContain('--model');
561
- expect(openCodeInvocations[2]).toContain(`--dir ${testDir}`);
562
- expect(openCodeInvocations[2]).toContain('--session ses-opencode-contract');
563
- expect(openCodeInvocations[2]).toContain('--model openai/gpt-5.4');
564
- expect(openCodeInvocations[3]).toContain(`--dir ${testDir}`);
565
- expect(openCodeInvocations[3]).toContain('--model openai/gpt-5.4');
566
- await expect(client.callTool('run', {
567
- prompt: 'opencode-invalid-reasoning',
568
- workFolder: testDir,
569
- model: 'opencode',
570
- reasoning_effort: 'high',
571
- })).rejects.toThrow(/reasoning_effort is not supported for opencode/i);
572
- });
573
- it('keeps key invalid-input errors stable', async () => {
574
- await expect(client.callTool('run', {
575
- prompt: 'missing workFolder',
576
- })).rejects.toThrow(/workFolder/i);
577
- await expect(client.callTool('run', {
578
- prompt: 'bad dir',
579
- workFolder: join(testDir, 'missing-dir'),
580
- })).rejects.toThrow(/does not exist/i);
581
- const promptFile = join(testDir, 'both.txt');
582
- writeFileSync(promptFile, 'test');
583
- await expect(client.callTool('run', {
584
- prompt: 'hello',
585
- prompt_file: promptFile,
586
- workFolder: testDir,
587
- })).rejects.toThrow(/both prompt and prompt_file/i);
588
- await expect(client.callTool('run', {
589
- workFolder: testDir,
590
- })).rejects.toThrow(/prompt or prompt_file/i);
591
- });
592
- it('keeps unknown PID errors stable for get_result, wait, and kill_process', async () => {
593
- await expect(client.callTool('get_result', { pid: 999999 })).rejects.toThrow(/PID 999999 not found/i);
594
- await expect(client.callTool('wait', { pids: [999999] })).rejects.toThrow(/PID 999999 not found/i);
595
- await expect(client.callTool('kill_process', { pid: 999999 })).rejects.toThrow(/PID 999999 not found/i);
596
- });
597
- it('preserves kill_process response shape for a running process', async () => {
598
- await client.disconnect();
599
- const slowMockPath = join(testDir, 'slow-claude');
600
- writeFileSync(slowMockPath, `#!/bin/bash
601
- prompt=""
602
- while [[ $# -gt 0 ]]; do
603
- case "$1" in
604
- -p|--prompt)
605
- prompt="$2"
606
- shift 2
607
- ;;
608
- *)
609
- shift
610
- ;;
611
- esac
612
- done
613
-
614
- if [[ "$prompt" == *"sleep"* ]]; then
615
- sleep 5
616
- fi
617
-
618
- echo "Command executed successfully"
619
- `);
620
- chmodSync(slowMockPath, 0o755);
621
- client = createTestClient({ claudeCliName: slowMockPath, debug: false });
622
- await client.connect();
623
- const runResponse = await client.callTool('run', {
624
- prompt: 'sleep for contract kill test',
625
- workFolder: testDir,
626
- });
627
- const runData = parseToolJson(runResponse);
628
- const killResponse = await client.callTool('kill_process', { pid: runData.pid });
629
- const killData = parseToolJson(killResponse);
630
- expect(killData).toEqual({
631
- pid: runData.pid,
632
- status: 'terminated',
633
- message: expect.any(String),
634
- });
635
- });
636
- });
@@ -1,32 +0,0 @@
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
- };