ai-cli-mcp 2.14.1 → 2.16.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/.github/dependabot.yml +28 -0
- package/.github/workflows/ci.yml +4 -1
- package/.github/workflows/dependency-review.yml +22 -0
- package/CHANGELOG.md +14 -0
- package/README.ja.md +83 -6
- package/README.md +83 -7
- package/dist/__tests__/app-cli.test.js +80 -5
- package/dist/__tests__/cli-bin-smoke.test.js +43 -0
- package/dist/__tests__/cli-builder.test.js +93 -15
- package/dist/__tests__/cli-process-service.test.js +162 -0
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/e2e.test.js +79 -52
- package/dist/__tests__/mcp-contract.test.js +162 -0
- package/dist/__tests__/parsers.test.js +224 -1
- package/dist/__tests__/peek.test.js +35 -0
- package/dist/__tests__/process-management.test.js +160 -1
- package/dist/__tests__/server.test.js +39 -9
- package/dist/__tests__/utils/opencode-mock.js +91 -0
- package/dist/__tests__/validation.test.js +40 -2
- package/dist/app/cli.js +47 -5
- package/dist/app/mcp.js +53 -4
- package/dist/cli-builder.js +67 -28
- package/dist/cli-parse.js +11 -5
- package/dist/cli-process-service.js +241 -20
- package/dist/cli-utils.js +14 -23
- package/dist/cli.js +6 -4
- package/dist/model-catalog.js +13 -1
- package/dist/parsers.js +242 -28
- package/dist/peek.js +56 -0
- package/dist/process-result.js +9 -2
- package/dist/process-service.js +103 -17
- package/dist/server.js +1 -2
- package/package.json +9 -6
- package/src/__tests__/app-cli.test.ts +95 -4
- package/src/__tests__/cli-bin-smoke.test.ts +62 -1
- package/src/__tests__/cli-builder.test.ts +111 -15
- package/src/__tests__/cli-process-service.test.ts +180 -0
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/e2e.test.ts +87 -55
- package/src/__tests__/mcp-contract.test.ts +188 -0
- package/src/__tests__/parsers.test.ts +260 -1
- package/src/__tests__/peek.test.ts +43 -0
- package/src/__tests__/process-management.test.ts +185 -1
- package/src/__tests__/server.test.ts +49 -13
- package/src/__tests__/utils/opencode-mock.ts +108 -0
- package/src/__tests__/validation.test.ts +48 -2
- package/src/app/cli.ts +52 -4
- package/src/app/mcp.ts +54 -4
- package/src/cli-builder.ts +91 -32
- package/src/cli-parse.ts +11 -5
- package/src/cli-process-service.ts +304 -17
- package/src/cli-utils.ts +37 -33
- package/src/cli.ts +6 -4
- package/src/model-catalog.ts +24 -1
- package/src/parsers.ts +299 -33
- package/src/peek.ts +88 -0
- package/src/process-result.ts +11 -2
- package/src/process-service.ts +134 -15
- package/src/server.ts +2 -2
- package/vitest.config.unit.ts +2 -3
|
@@ -20,6 +20,7 @@ describe('cli-utils doctor status', () => {
|
|
|
20
20
|
delete process.env.CODEX_CLI_NAME;
|
|
21
21
|
delete process.env.GEMINI_CLI_NAME;
|
|
22
22
|
delete process.env.FORGE_CLI_NAME;
|
|
23
|
+
delete process.env.OPENCODE_CLI_NAME;
|
|
23
24
|
process.env.PATH = '/mock/bin:/usr/bin';
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -51,6 +52,12 @@ describe('cli-utils doctor status', () => {
|
|
|
51
52
|
available: false,
|
|
52
53
|
lookup: 'path',
|
|
53
54
|
});
|
|
55
|
+
expect(status.opencode).toEqual({
|
|
56
|
+
configuredCommand: 'opencode',
|
|
57
|
+
resolvedPath: null,
|
|
58
|
+
available: false,
|
|
59
|
+
lookup: 'path',
|
|
60
|
+
});
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
it('does not mark non-executable PATH entries as available', async () => {
|
|
@@ -73,6 +80,12 @@ describe('cli-utils doctor status', () => {
|
|
|
73
80
|
available: false,
|
|
74
81
|
lookup: 'path',
|
|
75
82
|
});
|
|
83
|
+
expect(status.opencode).toEqual({
|
|
84
|
+
configuredCommand: 'opencode',
|
|
85
|
+
resolvedPath: null,
|
|
86
|
+
available: false,
|
|
87
|
+
lookup: 'path',
|
|
88
|
+
});
|
|
76
89
|
});
|
|
77
90
|
|
|
78
91
|
it('reports invalid relative env paths as doctor errors', async () => {
|
|
@@ -163,4 +176,25 @@ describe('cli-utils doctor status', () => {
|
|
|
163
176
|
});
|
|
164
177
|
expect(findForgeCli()).toBe('forge-custom');
|
|
165
178
|
});
|
|
179
|
+
|
|
180
|
+
it('supports OpenCode lookup via OPENCODE_CLI_NAME', async () => {
|
|
181
|
+
process.env.OPENCODE_CLI_NAME = 'opencode-custom';
|
|
182
|
+
mockAccessSync.mockImplementation((filePath) => {
|
|
183
|
+
if (filePath === '/mock/bin/opencode-custom') {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
throw new Error('not executable');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const { getCliDoctorStatus, findOpencodeCli } = await import('../cli-utils.js');
|
|
190
|
+
const status = getCliDoctorStatus();
|
|
191
|
+
|
|
192
|
+
expect(status.opencode).toEqual({
|
|
193
|
+
configuredCommand: 'opencode-custom',
|
|
194
|
+
resolvedPath: '/mock/bin/opencode-custom',
|
|
195
|
+
available: true,
|
|
196
|
+
lookup: 'env',
|
|
197
|
+
});
|
|
198
|
+
expect(findOpencodeCli()).toBe('opencode-custom');
|
|
199
|
+
});
|
|
166
200
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, readFileSync
|
|
2
|
+
import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { createTestClient, MCPTestClient } from './utils/mcp-client.js';
|
|
6
6
|
import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
|
|
7
|
+
import { createOpenCodeMock } from './utils/opencode-mock.js';
|
|
7
8
|
|
|
8
9
|
describe('Claude Code MCP E2E Tests', () => {
|
|
9
10
|
let client: MCPTestClient;
|
|
@@ -38,46 +39,17 @@ describe('Claude Code MCP E2E Tests', () => {
|
|
|
38
39
|
it('should register run tool', async () => {
|
|
39
40
|
const tools = await client.listTools();
|
|
40
41
|
|
|
41
|
-
expect(tools).toHaveLength(
|
|
42
|
+
expect(tools).toHaveLength(7);
|
|
42
43
|
const claudeCodeTool = tools.find((t: any) => t.name === 'run');
|
|
43
|
-
expect(claudeCodeTool).
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: {
|
|
49
|
-
prompt: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
description: expect.stringContaining('Either this or prompt_file is required'),
|
|
52
|
-
},
|
|
53
|
-
prompt_file: {
|
|
54
|
-
type: 'string',
|
|
55
|
-
description: expect.stringContaining('Path to a file containing the prompt'),
|
|
56
|
-
},
|
|
57
|
-
workFolder: {
|
|
58
|
-
type: 'string',
|
|
59
|
-
description: expect.stringContaining('working directory'),
|
|
60
|
-
},
|
|
61
|
-
model: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
description: expect.stringContaining('sonnet'),
|
|
64
|
-
},
|
|
65
|
-
reasoning_effort: {
|
|
66
|
-
type: 'string',
|
|
67
|
-
description: expect.stringContaining('model_reasoning_effort'),
|
|
68
|
-
},
|
|
69
|
-
session_id: {
|
|
70
|
-
type: 'string',
|
|
71
|
-
description: expect.stringContaining('session ID'),
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
required: ['workFolder'],
|
|
75
|
-
},
|
|
76
|
-
});
|
|
44
|
+
expect(claudeCodeTool.inputSchema.properties.model.description).toContain('sonnet');
|
|
45
|
+
expect(claudeCodeTool.inputSchema.properties.model.description).toContain('opencode');
|
|
46
|
+
expect(claudeCodeTool.inputSchema.properties.model.description).toContain('oc-<provider/model>');
|
|
47
|
+
expect(claudeCodeTool.inputSchema.properties.reasoning_effort.description).toContain('OpenCode');
|
|
77
48
|
|
|
78
49
|
// Verify other tools exist
|
|
79
50
|
expect(tools.some((t: any) => t.name === 'list_processes')).toBe(true);
|
|
80
51
|
expect(tools.some((t: any) => t.name === 'get_result')).toBe(true);
|
|
52
|
+
expect(tools.some((t: any) => t.name === 'peek')).toBe(true);
|
|
81
53
|
expect(tools.some((t: any) => t.name === 'kill_process')).toBe(true);
|
|
82
54
|
});
|
|
83
55
|
});
|
|
@@ -219,6 +191,78 @@ describe('Claude Code MCP E2E Tests', () => {
|
|
|
219
191
|
});
|
|
220
192
|
});
|
|
221
193
|
|
|
194
|
+
describe('OpenCode flows', () => {
|
|
195
|
+
it('should execute and resume OpenCode runs through the MCP client', async () => {
|
|
196
|
+
await client.disconnect();
|
|
197
|
+
|
|
198
|
+
const opencodeArgsLogPath = join(testDir, 'opencode-args.log');
|
|
199
|
+
const { scriptPath } = createOpenCodeMock(testDir, {
|
|
200
|
+
argsLogPath: opencodeArgsLogPath,
|
|
201
|
+
defaultSessionId: 'ses-opencode-e2e',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
client = createTestClient({
|
|
205
|
+
debug: false,
|
|
206
|
+
env: {
|
|
207
|
+
OPENCODE_CLI_NAME: scriptPath,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
await client.connect();
|
|
211
|
+
|
|
212
|
+
const runResponse = await client.callTool('run', {
|
|
213
|
+
prompt: 'e2e OpenCode initial prompt',
|
|
214
|
+
workFolder: testDir,
|
|
215
|
+
model: 'opencode',
|
|
216
|
+
});
|
|
217
|
+
const runData = JSON.parse(runResponse[0].text);
|
|
218
|
+
expect(runData.agent).toBe('opencode');
|
|
219
|
+
|
|
220
|
+
const initialWait = JSON.parse((await client.callTool('wait', { pids: [runData.pid], timeout: 5 }))[0].text);
|
|
221
|
+
expect(initialWait).toHaveLength(1);
|
|
222
|
+
expect(initialWait[0]).toMatchObject({
|
|
223
|
+
pid: runData.pid,
|
|
224
|
+
agent: 'opencode',
|
|
225
|
+
status: 'completed',
|
|
226
|
+
exitCode: 0,
|
|
227
|
+
model: 'opencode',
|
|
228
|
+
session_id: 'ses-opencode-e2e',
|
|
229
|
+
agentOutput: {
|
|
230
|
+
message: 'Initial: e2e OpenCode initial prompt',
|
|
231
|
+
session_id: 'ses-opencode-e2e',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const resumedResponse = await client.callTool('run', {
|
|
236
|
+
prompt: 'e2e OpenCode resumed prompt',
|
|
237
|
+
workFolder: testDir,
|
|
238
|
+
model: 'oc-openai/gpt-5.4',
|
|
239
|
+
session_id: 'ses-opencode-e2e',
|
|
240
|
+
});
|
|
241
|
+
const resumedRunData = JSON.parse(resumedResponse[0].text);
|
|
242
|
+
|
|
243
|
+
const resumedWait = JSON.parse((await client.callTool('wait', { pids: [resumedRunData.pid], timeout: 5 }))[0].text);
|
|
244
|
+
expect(resumedWait).toHaveLength(1);
|
|
245
|
+
expect(resumedWait[0]).toMatchObject({
|
|
246
|
+
pid: resumedRunData.pid,
|
|
247
|
+
agent: 'opencode',
|
|
248
|
+
status: 'completed',
|
|
249
|
+
exitCode: 0,
|
|
250
|
+
model: 'oc-openai/gpt-5.4',
|
|
251
|
+
session_id: 'ses-opencode-e2e',
|
|
252
|
+
agentOutput: {
|
|
253
|
+
message: 'Resumed model openai/gpt-5.4: e2e OpenCode resumed prompt',
|
|
254
|
+
session_id: 'ses-opencode-e2e',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const invocationLog = readFileSync(opencodeArgsLogPath, 'utf-8').trim().split('\n');
|
|
259
|
+
expect(invocationLog[0]).toContain(`--dir ${testDir}`);
|
|
260
|
+
expect(invocationLog[0]).not.toContain('--model');
|
|
261
|
+
expect(invocationLog[1]).toContain('--session ses-opencode-e2e');
|
|
262
|
+
expect(invocationLog[1]).toContain('--model openai/gpt-5.4');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
222
266
|
describe('Debug Mode', () => {
|
|
223
267
|
it('should log debug information when enabled', async () => {
|
|
224
268
|
// Debug logs go to stderr, which we capture in the client
|
|
@@ -250,30 +294,18 @@ describe('Integration Tests (Local Only)', () => {
|
|
|
250
294
|
rmSync(testDir, { recursive: true, force: true });
|
|
251
295
|
});
|
|
252
296
|
|
|
253
|
-
//
|
|
254
|
-
it.skip('should
|
|
297
|
+
// This smoke test only verifies that a real Claude CLI can be invoked.
|
|
298
|
+
it.skip('should invoke the real Claude CLI', async () => {
|
|
255
299
|
await client.connect();
|
|
256
|
-
|
|
257
|
-
const response = await client.callTool('run', {
|
|
258
|
-
prompt: 'Create a file called hello.txt with content "Hello from Claude"',
|
|
259
|
-
workFolder: testDir,
|
|
260
|
-
});
|
|
261
300
|
|
|
262
|
-
const filePath = join(testDir, 'hello.txt');
|
|
263
|
-
expect(existsSync(filePath)).toBe(true);
|
|
264
|
-
expect(readFileSync(filePath, 'utf-8')).toContain('Hello from Claude');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it.skip('should handle git operations with real Claude CLI', async () => {
|
|
268
|
-
await client.connect();
|
|
269
|
-
|
|
270
|
-
// Initialize git repo
|
|
271
301
|
const response = await client.callTool('run', {
|
|
272
|
-
prompt: '
|
|
302
|
+
prompt: 'Reply with hi',
|
|
273
303
|
workFolder: testDir,
|
|
274
304
|
});
|
|
275
305
|
|
|
276
|
-
expect(
|
|
277
|
-
|
|
306
|
+
expect(response).toEqual([{
|
|
307
|
+
type: 'text',
|
|
308
|
+
text: expect.stringContaining('pid'),
|
|
309
|
+
}]);
|
|
278
310
|
});
|
|
279
311
|
});
|
|
@@ -3,6 +3,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { cleanupSharedMock, getSharedMock } from './utils/persistent-mock.js';
|
|
6
|
+
import { createOpenCodeMock } from './utils/opencode-mock.js';
|
|
6
7
|
import { createTestClient, MCPTestClient } from './utils/mcp-client.js';
|
|
7
8
|
|
|
8
9
|
function parseToolJson(content: any): any {
|
|
@@ -95,6 +96,7 @@ describe('MCP Contract Tests', () => {
|
|
|
95
96
|
'get_result',
|
|
96
97
|
'kill_process',
|
|
97
98
|
'list_processes',
|
|
99
|
+
'peek',
|
|
98
100
|
'run',
|
|
99
101
|
'wait',
|
|
100
102
|
]);
|
|
@@ -109,6 +111,13 @@ describe('MCP Contract Tests', () => {
|
|
|
109
111
|
'session_id',
|
|
110
112
|
'workFolder',
|
|
111
113
|
]);
|
|
114
|
+
expect(runTool.description).toContain('OpenCode');
|
|
115
|
+
expect(runTool.inputSchema.properties.model.description).toContain('opencode');
|
|
116
|
+
expect(runTool.inputSchema.properties.model.description).toContain('oc-<provider/model>');
|
|
117
|
+
expect(runTool.inputSchema.properties.reasoning_effort.description).toContain('OpenCode do not support reasoning_effort');
|
|
118
|
+
expect(runTool.inputSchema.properties.session_id.description).toBe(
|
|
119
|
+
'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.'
|
|
120
|
+
);
|
|
112
121
|
|
|
113
122
|
const getResultTool = tools.find((tool: any) => tool.name === 'get_result');
|
|
114
123
|
expect(getResultTool.inputSchema.required).toEqual(['pid']);
|
|
@@ -124,6 +133,14 @@ describe('MCP Contract Tests', () => {
|
|
|
124
133
|
'timeout',
|
|
125
134
|
'verbose',
|
|
126
135
|
]);
|
|
136
|
+
|
|
137
|
+
const peekTool = tools.find((tool: any) => tool.name === 'peek');
|
|
138
|
+
expect(peekTool.inputSchema.required).toEqual(['pids']);
|
|
139
|
+
expect(Object.keys(peekTool.inputSchema.properties).sort()).toEqual([
|
|
140
|
+
'peek_time_sec',
|
|
141
|
+
'pids',
|
|
142
|
+
]);
|
|
143
|
+
expect(peekTool.description).toContain('One-shot');
|
|
127
144
|
});
|
|
128
145
|
|
|
129
146
|
it('preserves the stdio MCP smoke flow and response shapes', async () => {
|
|
@@ -470,6 +487,177 @@ printf '%s\n' '{"type":"system","session_id":"session-verbose-1"}'
|
|
|
470
487
|
).rejects.toThrow(/reasoning_effort is not supported for forge/i);
|
|
471
488
|
});
|
|
472
489
|
|
|
490
|
+
it('covers OpenCode end-to-end through the MCP process path', async () => {
|
|
491
|
+
await client.disconnect();
|
|
492
|
+
|
|
493
|
+
const opencodeArgsLogPath = join(testDir, 'opencode-args.log');
|
|
494
|
+
const { scriptPath: openCodeMockPath } = createOpenCodeMock(testDir, {
|
|
495
|
+
argsLogPath: opencodeArgsLogPath,
|
|
496
|
+
defaultSessionId: 'ses-opencode-contract',
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
client = createTestClient({
|
|
500
|
+
debug: false,
|
|
501
|
+
env: {
|
|
502
|
+
OPENCODE_CLI_NAME: openCodeMockPath,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
await client.connect();
|
|
506
|
+
|
|
507
|
+
const initialRunResponse = await client.callTool('run', {
|
|
508
|
+
prompt: 'opencode-initial-prompt',
|
|
509
|
+
workFolder: testDir,
|
|
510
|
+
model: 'opencode',
|
|
511
|
+
});
|
|
512
|
+
const initialRunData = parseToolJson(initialRunResponse);
|
|
513
|
+
|
|
514
|
+
expect(initialRunData).toEqual({
|
|
515
|
+
pid: expect.any(Number),
|
|
516
|
+
status: 'started',
|
|
517
|
+
agent: 'opencode',
|
|
518
|
+
message: expect.any(String),
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const initialWaitResponse = await client.callTool('wait', { pids: [initialRunData.pid], timeout: 5 });
|
|
522
|
+
const initialWaitData = parseToolJson(initialWaitResponse);
|
|
523
|
+
|
|
524
|
+
expect(initialWaitData).toHaveLength(1);
|
|
525
|
+
expect(initialWaitData[0]).toMatchObject({
|
|
526
|
+
pid: initialRunData.pid,
|
|
527
|
+
agent: 'opencode',
|
|
528
|
+
status: 'completed',
|
|
529
|
+
exitCode: 0,
|
|
530
|
+
model: 'opencode',
|
|
531
|
+
session_id: 'ses-opencode-contract',
|
|
532
|
+
agentOutput: {
|
|
533
|
+
message: 'Initial: opencode-initial-prompt',
|
|
534
|
+
session_id: 'ses-opencode-contract',
|
|
535
|
+
tokens: { total: 11833 },
|
|
536
|
+
cost: 0,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const resumedDefaultRunResponse = await client.callTool('run', {
|
|
541
|
+
prompt: 'opencode-resume-default',
|
|
542
|
+
workFolder: testDir,
|
|
543
|
+
model: 'opencode',
|
|
544
|
+
session_id: 'ses-opencode-contract',
|
|
545
|
+
});
|
|
546
|
+
const resumedDefaultRunData = parseToolJson(resumedDefaultRunResponse);
|
|
547
|
+
|
|
548
|
+
const resumedDefaultWaitResponse = await client.callTool('wait', { pids: [resumedDefaultRunData.pid], timeout: 5 });
|
|
549
|
+
const resumedDefaultWaitData = parseToolJson(resumedDefaultWaitResponse);
|
|
550
|
+
|
|
551
|
+
expect(resumedDefaultWaitData).toHaveLength(1);
|
|
552
|
+
expect(resumedDefaultWaitData[0]).toMatchObject({
|
|
553
|
+
pid: resumedDefaultRunData.pid,
|
|
554
|
+
agent: 'opencode',
|
|
555
|
+
status: 'completed',
|
|
556
|
+
exitCode: 0,
|
|
557
|
+
model: 'opencode',
|
|
558
|
+
session_id: 'ses-opencode-contract',
|
|
559
|
+
agentOutput: {
|
|
560
|
+
message: 'Resumed: opencode-resume-default',
|
|
561
|
+
session_id: 'ses-opencode-contract',
|
|
562
|
+
tokens: { total: 11833 },
|
|
563
|
+
cost: 0,
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const resumedExplicitRunResponse = await client.callTool('run', {
|
|
568
|
+
prompt: 'opencode-resume-explicit',
|
|
569
|
+
workFolder: testDir,
|
|
570
|
+
model: 'oc-openai/gpt-5.4',
|
|
571
|
+
session_id: 'ses-opencode-contract',
|
|
572
|
+
});
|
|
573
|
+
const resumedExplicitRunData = parseToolJson(resumedExplicitRunResponse);
|
|
574
|
+
|
|
575
|
+
const resumedExplicitWaitResponse = await client.callTool('wait', { pids: [resumedExplicitRunData.pid], timeout: 5 });
|
|
576
|
+
const resumedExplicitWaitData = parseToolJson(resumedExplicitWaitResponse);
|
|
577
|
+
|
|
578
|
+
expect(resumedExplicitWaitData).toHaveLength(1);
|
|
579
|
+
expect(resumedExplicitWaitData[0]).toMatchObject({
|
|
580
|
+
pid: resumedExplicitRunData.pid,
|
|
581
|
+
agent: 'opencode',
|
|
582
|
+
status: 'completed',
|
|
583
|
+
exitCode: 0,
|
|
584
|
+
model: 'oc-openai/gpt-5.4',
|
|
585
|
+
session_id: 'ses-opencode-contract',
|
|
586
|
+
agentOutput: {
|
|
587
|
+
message: 'Resumed model openai/gpt-5.4: opencode-resume-explicit',
|
|
588
|
+
session_id: 'ses-opencode-contract',
|
|
589
|
+
tokens: { total: 11833 },
|
|
590
|
+
cost: 0,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const failedRunResponse = await client.callTool('run', {
|
|
595
|
+
prompt: 'please fail',
|
|
596
|
+
workFolder: testDir,
|
|
597
|
+
model: 'oc-openai/gpt-5.4',
|
|
598
|
+
});
|
|
599
|
+
const failedRunData = parseToolJson(failedRunResponse);
|
|
600
|
+
|
|
601
|
+
const compactFailedWait = parseToolJson(await client.callTool('wait', { pids: [failedRunData.pid], timeout: 5 }));
|
|
602
|
+
expect(compactFailedWait).toHaveLength(1);
|
|
603
|
+
expect(compactFailedWait[0]).toMatchObject({
|
|
604
|
+
pid: failedRunData.pid,
|
|
605
|
+
agent: 'opencode',
|
|
606
|
+
status: 'failed',
|
|
607
|
+
exitCode: 7,
|
|
608
|
+
model: 'oc-openai/gpt-5.4',
|
|
609
|
+
session_id: 'ses-opencode-contract',
|
|
610
|
+
stdout: expect.stringContaining('Partial failure output'),
|
|
611
|
+
stderr: expect.stringContaining('OpenCode failed for openai/gpt-5.4'),
|
|
612
|
+
});
|
|
613
|
+
expect(compactFailedWait[0]).not.toHaveProperty('agentOutput');
|
|
614
|
+
|
|
615
|
+
const verboseFailedResult = parseToolJson(await client.callTool('get_result', { pid: failedRunData.pid, verbose: true }));
|
|
616
|
+
expect(verboseFailedResult).toMatchObject({
|
|
617
|
+
pid: failedRunData.pid,
|
|
618
|
+
agent: 'opencode',
|
|
619
|
+
status: 'failed',
|
|
620
|
+
exitCode: 7,
|
|
621
|
+
model: 'oc-openai/gpt-5.4',
|
|
622
|
+
session_id: 'ses-opencode-contract',
|
|
623
|
+
stdout: expect.stringContaining('Partial failure output'),
|
|
624
|
+
stderr: expect.stringContaining('OpenCode failed for openai/gpt-5.4'),
|
|
625
|
+
agentOutput: {
|
|
626
|
+
message: 'Partial failure output',
|
|
627
|
+
session_id: 'ses-opencode-contract',
|
|
628
|
+
tokens: { total: 42 },
|
|
629
|
+
cost: 0,
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const openCodeInvocations = readFileSync(opencodeArgsLogPath, 'utf-8').trim().split('\n');
|
|
634
|
+
expect(openCodeInvocations).toHaveLength(4);
|
|
635
|
+
expect(openCodeInvocations[0]).toContain('run --format json');
|
|
636
|
+
expect(openCodeInvocations[0]).toContain(`--dir ${testDir}`);
|
|
637
|
+
expect(openCodeInvocations[0]).not.toContain('--session');
|
|
638
|
+
expect(openCodeInvocations[0]).not.toContain('--model');
|
|
639
|
+
|
|
640
|
+
expect(openCodeInvocations[1]).toContain(`--dir ${testDir}`);
|
|
641
|
+
expect(openCodeInvocations[1]).toContain('--session ses-opencode-contract');
|
|
642
|
+
expect(openCodeInvocations[1]).not.toContain('--model');
|
|
643
|
+
|
|
644
|
+
expect(openCodeInvocations[2]).toContain(`--dir ${testDir}`);
|
|
645
|
+
expect(openCodeInvocations[2]).toContain('--session ses-opencode-contract');
|
|
646
|
+
expect(openCodeInvocations[2]).toContain('--model openai/gpt-5.4');
|
|
647
|
+
|
|
648
|
+
expect(openCodeInvocations[3]).toContain(`--dir ${testDir}`);
|
|
649
|
+
expect(openCodeInvocations[3]).toContain('--model openai/gpt-5.4');
|
|
650
|
+
|
|
651
|
+
await expect(
|
|
652
|
+
client.callTool('run', {
|
|
653
|
+
prompt: 'opencode-invalid-reasoning',
|
|
654
|
+
workFolder: testDir,
|
|
655
|
+
model: 'opencode',
|
|
656
|
+
reasoning_effort: 'high',
|
|
657
|
+
})
|
|
658
|
+
).rejects.toThrow(/reasoning_effort is not supported for opencode/i);
|
|
659
|
+
});
|
|
660
|
+
|
|
473
661
|
it('keeps key invalid-input errors stable', async () => {
|
|
474
662
|
await expect(
|
|
475
663
|
client.callTool('run', {
|