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.
- package/CHANGELOG.md +26 -0
- package/README.ja.md +34 -8
- package/README.md +41 -8
- package/dist/app/cli.js +1 -0
- package/dist/app/mcp.js +64 -12
- package/dist/cli-builder.js +13 -6
- package/dist/cli-process-service.js +76 -91
- package/dist/cli-utils.js +6 -0
- package/dist/cli.js +1 -1
- package/dist/model-catalog.js +3 -2
- package/dist/parsers.js +8 -2
- package/package.json +27 -3
- package/server.json +3 -3
- package/.gemini/settings.json +0 -11
- package/.github/dependabot.yml +0 -28
- package/.github/pull_request_template.md +0 -28
- package/.github/workflows/ci.yml +0 -34
- package/.github/workflows/dependency-review.yml +0 -22
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/test.yml +0 -20
- package/.github/workflows/watch-session-prs.yml +0 -276
- package/.husky/pre-commit +0 -1
- package/.mcp.json +0 -11
- package/.releaserc.json +0 -18
- package/.vscode/settings.json +0 -3
- package/CONTRIBUTING.md +0 -81
- package/dist/__tests__/app-cli.test.js +0 -392
- package/dist/__tests__/cli-bin-smoke.test.js +0 -101
- package/dist/__tests__/cli-builder.test.js +0 -442
- package/dist/__tests__/cli-process-service.test.js +0 -655
- package/dist/__tests__/cli-utils.test.js +0 -171
- package/dist/__tests__/e2e.test.js +0 -256
- package/dist/__tests__/edge-cases.test.js +0 -130
- package/dist/__tests__/error-cases.test.js +0 -292
- package/dist/__tests__/mcp-contract.test.js +0 -636
- package/dist/__tests__/mocks.js +0 -32
- package/dist/__tests__/model-alias.test.js +0 -36
- package/dist/__tests__/parsers.test.js +0 -646
- package/dist/__tests__/peek.test.js +0 -36
- package/dist/__tests__/process-management.test.js +0 -949
- package/dist/__tests__/server.test.js +0 -809
- package/dist/__tests__/setup.js +0 -11
- package/dist/__tests__/utils/claude-mock.js +0 -80
- package/dist/__tests__/utils/mcp-client.js +0 -121
- package/dist/__tests__/utils/opencode-mock.js +0 -91
- package/dist/__tests__/utils/persistent-mock.js +0 -28
- package/dist/__tests__/utils/test-helpers.js +0 -11
- package/dist/__tests__/validation.test.js +0 -308
- package/dist/__tests__/version-print.test.js +0 -65
- package/dist/__tests__/wait.test.js +0 -260
- package/docs/RELEASE_CHECKLIST.md +0 -65
- package/docs/cli-architecture.md +0 -275
- package/docs/concept.md +0 -154
- package/docs/development.md +0 -156
- package/docs/e2e-testing.md +0 -148
- package/docs/prd.md +0 -146
- package/docs/session-stacking.md +0 -67
- package/src/__tests__/app-cli.test.ts +0 -495
- package/src/__tests__/cli-bin-smoke.test.ts +0 -136
- package/src/__tests__/cli-builder.test.ts +0 -549
- package/src/__tests__/cli-process-service.test.ts +0 -759
- package/src/__tests__/cli-utils.test.ts +0 -200
- package/src/__tests__/e2e.test.ts +0 -311
- package/src/__tests__/edge-cases.test.ts +0 -176
- package/src/__tests__/error-cases.test.ts +0 -370
- package/src/__tests__/mcp-contract.test.ts +0 -755
- package/src/__tests__/mocks.ts +0 -35
- package/src/__tests__/model-alias.test.ts +0 -44
- package/src/__tests__/parsers.test.ts +0 -730
- package/src/__tests__/peek.test.ts +0 -44
- package/src/__tests__/process-management.test.ts +0 -1129
- package/src/__tests__/server.test.ts +0 -1020
- package/src/__tests__/setup.ts +0 -13
- package/src/__tests__/utils/claude-mock.ts +0 -87
- package/src/__tests__/utils/mcp-client.ts +0 -159
- package/src/__tests__/utils/opencode-mock.ts +0 -108
- package/src/__tests__/utils/persistent-mock.ts +0 -33
- package/src/__tests__/utils/test-helpers.ts +0 -13
- package/src/__tests__/validation.test.ts +0 -369
- package/src/__tests__/version-print.test.ts +0 -81
- package/src/__tests__/wait.test.ts +0 -302
- package/src/app/cli.ts +0 -424
- package/src/app/mcp.ts +0 -466
- package/src/bin/ai-cli-mcp.ts +0 -7
- package/src/bin/ai-cli.ts +0 -11
- package/src/cli-builder.ts +0 -274
- package/src/cli-parse.ts +0 -105
- package/src/cli-process-service.ts +0 -709
- package/src/cli-utils.ts +0 -258
- package/src/cli.ts +0 -124
- package/src/model-catalog.ts +0 -87
- package/src/parsers.ts +0 -965
- package/src/peek.ts +0 -95
- package/src/process-result.ts +0 -88
- package/src/process-service.ts +0 -368
- package/src/server.ts +0 -10
- package/tsconfig.json +0 -16
- package/vitest.config.e2e.ts +0 -27
- package/vitest.config.ts +0 -22
- 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
|
-
});
|
package/dist/__tests__/mocks.js
DELETED
|
@@ -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
|
-
};
|