ai-cli-mcp 2.14.0 → 2.15.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 +25 -6
- package/README.md +25 -7
- package/dist/__tests__/app-cli.test.js +24 -4
- package/dist/__tests__/cli-bin-smoke.test.js +43 -0
- package/dist/__tests__/cli-builder.test.js +92 -14
- package/dist/__tests__/cli-process-service.test.js +187 -0
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/e2e.test.js +77 -51
- package/dist/__tests__/mcp-contract.test.js +154 -0
- package/dist/__tests__/parsers.test.js +62 -1
- package/dist/__tests__/process-management.test.js +1 -1
- package/dist/__tests__/server.test.js +35 -6
- package/dist/__tests__/utils/opencode-mock.js +91 -0
- package/dist/__tests__/validation.test.js +40 -2
- package/dist/app/cli.js +4 -4
- package/dist/app/mcp.js +8 -4
- package/dist/cli-builder.js +66 -27
- package/dist/cli-parse.js +11 -5
- package/dist/cli-process-service.js +158 -25
- package/dist/cli-utils.js +14 -23
- package/dist/cli.js +6 -4
- package/dist/model-catalog.js +13 -1
- package/dist/parsers.js +57 -26
- package/dist/process-result.js +9 -2
- package/dist/process-service.js +23 -17
- package/dist/server.js +1 -2
- package/package.json +9 -6
- package/src/__tests__/app-cli.test.ts +24 -4
- package/src/__tests__/cli-bin-smoke.test.ts +62 -1
- package/src/__tests__/cli-builder.test.ts +110 -14
- package/src/__tests__/cli-process-service.test.ts +217 -0
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/e2e.test.ts +85 -54
- package/src/__tests__/mcp-contract.test.ts +179 -0
- package/src/__tests__/parsers.test.ts +73 -1
- package/src/__tests__/process-management.test.ts +1 -1
- package/src/__tests__/server.test.ts +45 -10
- package/src/__tests__/utils/opencode-mock.ts +108 -0
- package/src/__tests__/validation.test.ts +48 -2
- package/src/app/cli.ts +4 -4
- package/src/app/mcp.ts +8 -4
- package/src/cli-builder.ts +90 -31
- package/src/cli-parse.ts +11 -5
- package/src/cli-process-service.ts +193 -22
- package/src/cli-utils.ts +37 -33
- package/src/cli.ts +6 -4
- package/src/model-catalog.ts +24 -1
- package/src/parsers.ts +77 -31
- package/src/process-result.ts +11 -2
- package/src/process-service.ts +28 -15
- package/src/server.ts +2 -2
- package/vitest.config.unit.ts +2 -3
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { CliProcessService } from '../cli-process-service.js';
|
|
6
|
+
import { createOpenCodeMock } from './utils/opencode-mock.js';
|
|
6
7
|
|
|
7
8
|
function createMockCliScript(dir: string, name: string, options: { ignoreSigterm?: boolean } = {}): string {
|
|
8
9
|
const scriptPath = join(dir, name);
|
|
@@ -66,6 +67,7 @@ describe('CliProcessService', () => {
|
|
|
66
67
|
codex: scriptPath,
|
|
67
68
|
gemini: scriptPath,
|
|
68
69
|
forge: scriptPath,
|
|
70
|
+
opencode: scriptPath,
|
|
69
71
|
},
|
|
70
72
|
});
|
|
71
73
|
|
|
@@ -145,6 +147,7 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
|
145
147
|
codex: scriptPath,
|
|
146
148
|
gemini: scriptPath,
|
|
147
149
|
forge: scriptPath,
|
|
150
|
+
opencode: scriptPath,
|
|
148
151
|
},
|
|
149
152
|
});
|
|
150
153
|
|
|
@@ -255,6 +258,7 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
|
255
258
|
codex: scriptPath,
|
|
256
259
|
gemini: scriptPath,
|
|
257
260
|
forge: scriptPath,
|
|
261
|
+
opencode: scriptPath,
|
|
258
262
|
},
|
|
259
263
|
});
|
|
260
264
|
|
|
@@ -294,6 +298,7 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
|
294
298
|
codex: '/bin/sh',
|
|
295
299
|
gemini: '/bin/sh',
|
|
296
300
|
forge: '/bin/sh',
|
|
301
|
+
opencode: '/bin/sh',
|
|
297
302
|
},
|
|
298
303
|
});
|
|
299
304
|
|
|
@@ -334,6 +339,113 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
|
334
339
|
killSpy.mockRestore();
|
|
335
340
|
});
|
|
336
341
|
|
|
342
|
+
it('lists processes without crashing when a tracked work folder has been deleted', async () => {
|
|
343
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
344
|
+
tempDirs.push(root);
|
|
345
|
+
const stateDir = join(root, 'state');
|
|
346
|
+
const workFolder = join(root, 'deleted-project');
|
|
347
|
+
mkdirSync(workFolder, { recursive: true });
|
|
348
|
+
|
|
349
|
+
const pid = 45678;
|
|
350
|
+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
|
|
351
|
+
mkdirSync(processDir, { recursive: true });
|
|
352
|
+
|
|
353
|
+
writeFileSync(
|
|
354
|
+
join(processDir, 'meta.json'),
|
|
355
|
+
JSON.stringify({
|
|
356
|
+
pid,
|
|
357
|
+
prompt: 'deleted cwd',
|
|
358
|
+
workFolder,
|
|
359
|
+
toolType: 'claude',
|
|
360
|
+
startTime: new Date().toISOString(),
|
|
361
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
362
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
363
|
+
status: 'running',
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
rmSync(workFolder, { recursive: true, force: true });
|
|
368
|
+
|
|
369
|
+
const service = new CliProcessService({
|
|
370
|
+
stateDir,
|
|
371
|
+
cliPaths: {
|
|
372
|
+
claude: '/bin/sh',
|
|
373
|
+
codex: '/bin/sh',
|
|
374
|
+
gemini: '/bin/sh',
|
|
375
|
+
forge: '/bin/sh',
|
|
376
|
+
opencode: '/bin/sh',
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const killSpy = vi.spyOn(globalThis.process, 'kill').mockImplementation((target: number, signal?: string | number) => {
|
|
381
|
+
if (signal === 0 && target === pid) {
|
|
382
|
+
throw Object.assign(new Error('not running'), { code: 'ESRCH' });
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const listed = await service.listProcesses();
|
|
388
|
+
|
|
389
|
+
expect(listed).toEqual([
|
|
390
|
+
{
|
|
391
|
+
pid,
|
|
392
|
+
agent: 'claude',
|
|
393
|
+
status: 'completed',
|
|
394
|
+
},
|
|
395
|
+
]);
|
|
396
|
+
expect(JSON.parse(readFileSync(join(processDir, 'meta.json'), 'utf-8')).status).toBe('completed');
|
|
397
|
+
killSpy.mockRestore();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('cleans up finished process directories even when their work folder has been deleted', async () => {
|
|
401
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
402
|
+
tempDirs.push(root);
|
|
403
|
+
const stateDir = join(root, 'state');
|
|
404
|
+
const workFolder = join(root, 'deleted-finished-project');
|
|
405
|
+
mkdirSync(workFolder, { recursive: true });
|
|
406
|
+
|
|
407
|
+
const pid = 56789;
|
|
408
|
+
const cwdDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)));
|
|
409
|
+
const processDir = join(cwdDir, String(pid));
|
|
410
|
+
mkdirSync(processDir, { recursive: true });
|
|
411
|
+
|
|
412
|
+
writeFileSync(
|
|
413
|
+
join(processDir, 'meta.json'),
|
|
414
|
+
JSON.stringify({
|
|
415
|
+
pid,
|
|
416
|
+
prompt: 'done',
|
|
417
|
+
workFolder,
|
|
418
|
+
toolType: 'claude',
|
|
419
|
+
startTime: new Date().toISOString(),
|
|
420
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
421
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
422
|
+
status: 'completed',
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
rmSync(workFolder, { recursive: true, force: true });
|
|
427
|
+
|
|
428
|
+
const service = new CliProcessService({
|
|
429
|
+
stateDir,
|
|
430
|
+
cliPaths: {
|
|
431
|
+
claude: '/bin/sh',
|
|
432
|
+
codex: '/bin/sh',
|
|
433
|
+
gemini: '/bin/sh',
|
|
434
|
+
forge: '/bin/sh',
|
|
435
|
+
opencode: '/bin/sh',
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const result = await service.cleanupProcesses();
|
|
440
|
+
|
|
441
|
+
expect(result).toEqual({
|
|
442
|
+
removed: 1,
|
|
443
|
+
message: 'Removed 1 processes',
|
|
444
|
+
});
|
|
445
|
+
expect(existsSync(processDir)).toBe(false);
|
|
446
|
+
expect(existsSync(cwdDir)).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
337
449
|
it('cleans up completed and failed process directories but preserves running ones', async () => {
|
|
338
450
|
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
339
451
|
tempDirs.push(root);
|
|
@@ -397,6 +509,7 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
|
397
509
|
codex: '/bin/sh',
|
|
398
510
|
gemini: '/bin/sh',
|
|
399
511
|
forge: '/bin/sh',
|
|
512
|
+
opencode: '/bin/sh',
|
|
400
513
|
},
|
|
401
514
|
});
|
|
402
515
|
|
|
@@ -459,6 +572,7 @@ Forge assistant reply
|
|
|
459
572
|
codex: '/bin/sh',
|
|
460
573
|
gemini: '/bin/sh',
|
|
461
574
|
forge: '/bin/sh',
|
|
575
|
+
opencode: '/bin/sh',
|
|
462
576
|
},
|
|
463
577
|
});
|
|
464
578
|
|
|
@@ -470,4 +584,107 @@ Forge assistant reply
|
|
|
470
584
|
session_id: 'forge-conv-1',
|
|
471
585
|
});
|
|
472
586
|
});
|
|
587
|
+
|
|
588
|
+
it('parses successful OpenCode detached runs from stdout only', async () => {
|
|
589
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
590
|
+
tempDirs.push(root);
|
|
591
|
+
const stateDir = join(root, 'state');
|
|
592
|
+
const workFolder = join(root, 'opencode-project');
|
|
593
|
+
mkdirSync(workFolder, { recursive: true });
|
|
594
|
+
const argsLogPath = join(root, 'opencode-args.log');
|
|
595
|
+
const { scriptPath } = createOpenCodeMock(root, { argsLogPath });
|
|
596
|
+
|
|
597
|
+
const service = new CliProcessService({
|
|
598
|
+
stateDir,
|
|
599
|
+
cliPaths: {
|
|
600
|
+
claude: '/bin/sh',
|
|
601
|
+
codex: '/bin/sh',
|
|
602
|
+
gemini: '/bin/sh',
|
|
603
|
+
forge: '/bin/sh',
|
|
604
|
+
opencode: scriptPath,
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const runResult = await service.startProcess({
|
|
609
|
+
prompt: 'hello opencode',
|
|
610
|
+
cwd: workFolder,
|
|
611
|
+
model: 'opencode',
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const waited = await service.waitForProcesses([runResult.pid], 5);
|
|
615
|
+
expect(waited).toHaveLength(1);
|
|
616
|
+
expect(waited[0]).toMatchObject({
|
|
617
|
+
pid: runResult.pid,
|
|
618
|
+
agent: 'opencode',
|
|
619
|
+
status: 'completed',
|
|
620
|
+
exitCode: 0,
|
|
621
|
+
model: 'opencode',
|
|
622
|
+
session_id: 'ses-opencode-default',
|
|
623
|
+
agentOutput: {
|
|
624
|
+
message: 'Initial: hello opencode',
|
|
625
|
+
session_id: 'ses-opencode-default',
|
|
626
|
+
tokens: { total: 11833 },
|
|
627
|
+
cost: 0,
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
expect(waited[0]).not.toHaveProperty('stdout');
|
|
631
|
+
expect(waited[0]).not.toHaveProperty('stderr');
|
|
632
|
+
expect(readFileSync(argsLogPath, 'utf8')).toContain(`--dir ${workFolder}`);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('preserves raw stdout and stderr for failed detached OpenCode runs', async () => {
|
|
636
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
637
|
+
tempDirs.push(root);
|
|
638
|
+
const stateDir = join(root, 'state');
|
|
639
|
+
const workFolder = join(root, 'opencode-fail-project');
|
|
640
|
+
mkdirSync(workFolder, { recursive: true });
|
|
641
|
+
const { scriptPath } = createOpenCodeMock(root);
|
|
642
|
+
|
|
643
|
+
const service = new CliProcessService({
|
|
644
|
+
stateDir,
|
|
645
|
+
cliPaths: {
|
|
646
|
+
claude: '/bin/sh',
|
|
647
|
+
codex: '/bin/sh',
|
|
648
|
+
gemini: '/bin/sh',
|
|
649
|
+
forge: '/bin/sh',
|
|
650
|
+
opencode: scriptPath,
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const runResult = await service.startProcess({
|
|
655
|
+
prompt: 'please fail',
|
|
656
|
+
cwd: workFolder,
|
|
657
|
+
model: 'oc-openai/gpt-5.4',
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const [compactResult] = await service.waitForProcesses([runResult.pid], 5);
|
|
661
|
+
expect(compactResult).toMatchObject({
|
|
662
|
+
pid: runResult.pid,
|
|
663
|
+
agent: 'opencode',
|
|
664
|
+
status: 'failed',
|
|
665
|
+
exitCode: 7,
|
|
666
|
+
model: 'oc-openai/gpt-5.4',
|
|
667
|
+
session_id: 'ses-opencode-default',
|
|
668
|
+
stdout: expect.stringContaining('Partial failure output'),
|
|
669
|
+
stderr: expect.stringContaining('OpenCode failed for openai/gpt-5.4'),
|
|
670
|
+
});
|
|
671
|
+
expect(compactResult).not.toHaveProperty('agentOutput');
|
|
672
|
+
|
|
673
|
+
const verboseResult = await service.getProcessResult(runResult.pid, true);
|
|
674
|
+
expect(verboseResult).toMatchObject({
|
|
675
|
+
pid: runResult.pid,
|
|
676
|
+
agent: 'opencode',
|
|
677
|
+
status: 'failed',
|
|
678
|
+
exitCode: 7,
|
|
679
|
+
session_id: 'ses-opencode-default',
|
|
680
|
+
stdout: expect.stringContaining('Partial failure output'),
|
|
681
|
+
stderr: expect.stringContaining('OpenCode failed for openai/gpt-5.4'),
|
|
682
|
+
agentOutput: {
|
|
683
|
+
message: 'Partial failure output',
|
|
684
|
+
session_id: 'ses-opencode-default',
|
|
685
|
+
tokens: { total: 42 },
|
|
686
|
+
cost: 0,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
});
|
|
473
690
|
});
|
|
@@ -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;
|
|
@@ -40,40 +41,10 @@ describe('Claude Code MCP E2E Tests', () => {
|
|
|
40
41
|
|
|
41
42
|
expect(tools).toHaveLength(6);
|
|
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);
|
|
@@ -219,6 +190,78 @@ describe('Claude Code MCP E2E Tests', () => {
|
|
|
219
190
|
});
|
|
220
191
|
});
|
|
221
192
|
|
|
193
|
+
describe('OpenCode flows', () => {
|
|
194
|
+
it('should execute and resume OpenCode runs through the MCP client', async () => {
|
|
195
|
+
await client.disconnect();
|
|
196
|
+
|
|
197
|
+
const opencodeArgsLogPath = join(testDir, 'opencode-args.log');
|
|
198
|
+
const { scriptPath } = createOpenCodeMock(testDir, {
|
|
199
|
+
argsLogPath: opencodeArgsLogPath,
|
|
200
|
+
defaultSessionId: 'ses-opencode-e2e',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
client = createTestClient({
|
|
204
|
+
debug: false,
|
|
205
|
+
env: {
|
|
206
|
+
OPENCODE_CLI_NAME: scriptPath,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
await client.connect();
|
|
210
|
+
|
|
211
|
+
const runResponse = await client.callTool('run', {
|
|
212
|
+
prompt: 'e2e OpenCode initial prompt',
|
|
213
|
+
workFolder: testDir,
|
|
214
|
+
model: 'opencode',
|
|
215
|
+
});
|
|
216
|
+
const runData = JSON.parse(runResponse[0].text);
|
|
217
|
+
expect(runData.agent).toBe('opencode');
|
|
218
|
+
|
|
219
|
+
const initialWait = JSON.parse((await client.callTool('wait', { pids: [runData.pid], timeout: 5 }))[0].text);
|
|
220
|
+
expect(initialWait).toHaveLength(1);
|
|
221
|
+
expect(initialWait[0]).toMatchObject({
|
|
222
|
+
pid: runData.pid,
|
|
223
|
+
agent: 'opencode',
|
|
224
|
+
status: 'completed',
|
|
225
|
+
exitCode: 0,
|
|
226
|
+
model: 'opencode',
|
|
227
|
+
session_id: 'ses-opencode-e2e',
|
|
228
|
+
agentOutput: {
|
|
229
|
+
message: 'Initial: e2e OpenCode initial prompt',
|
|
230
|
+
session_id: 'ses-opencode-e2e',
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const resumedResponse = await client.callTool('run', {
|
|
235
|
+
prompt: 'e2e OpenCode resumed prompt',
|
|
236
|
+
workFolder: testDir,
|
|
237
|
+
model: 'oc-openai/gpt-5.4',
|
|
238
|
+
session_id: 'ses-opencode-e2e',
|
|
239
|
+
});
|
|
240
|
+
const resumedRunData = JSON.parse(resumedResponse[0].text);
|
|
241
|
+
|
|
242
|
+
const resumedWait = JSON.parse((await client.callTool('wait', { pids: [resumedRunData.pid], timeout: 5 }))[0].text);
|
|
243
|
+
expect(resumedWait).toHaveLength(1);
|
|
244
|
+
expect(resumedWait[0]).toMatchObject({
|
|
245
|
+
pid: resumedRunData.pid,
|
|
246
|
+
agent: 'opencode',
|
|
247
|
+
status: 'completed',
|
|
248
|
+
exitCode: 0,
|
|
249
|
+
model: 'oc-openai/gpt-5.4',
|
|
250
|
+
session_id: 'ses-opencode-e2e',
|
|
251
|
+
agentOutput: {
|
|
252
|
+
message: 'Resumed model openai/gpt-5.4: e2e OpenCode resumed prompt',
|
|
253
|
+
session_id: 'ses-opencode-e2e',
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const invocationLog = readFileSync(opencodeArgsLogPath, 'utf-8').trim().split('\n');
|
|
258
|
+
expect(invocationLog[0]).toContain(`--dir ${testDir}`);
|
|
259
|
+
expect(invocationLog[0]).not.toContain('--model');
|
|
260
|
+
expect(invocationLog[1]).toContain('--session ses-opencode-e2e');
|
|
261
|
+
expect(invocationLog[1]).toContain('--model openai/gpt-5.4');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
222
265
|
describe('Debug Mode', () => {
|
|
223
266
|
it('should log debug information when enabled', async () => {
|
|
224
267
|
// Debug logs go to stderr, which we capture in the client
|
|
@@ -250,30 +293,18 @@ describe('Integration Tests (Local Only)', () => {
|
|
|
250
293
|
rmSync(testDir, { recursive: true, force: true });
|
|
251
294
|
});
|
|
252
295
|
|
|
253
|
-
//
|
|
254
|
-
it.skip('should
|
|
296
|
+
// This smoke test only verifies that a real Claude CLI can be invoked.
|
|
297
|
+
it.skip('should invoke the real Claude CLI', async () => {
|
|
255
298
|
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
299
|
|
|
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
300
|
const response = await client.callTool('run', {
|
|
272
|
-
prompt: '
|
|
301
|
+
prompt: 'Reply with hi',
|
|
273
302
|
workFolder: testDir,
|
|
274
303
|
});
|
|
275
304
|
|
|
276
|
-
expect(
|
|
277
|
-
|
|
305
|
+
expect(response).toEqual([{
|
|
306
|
+
type: 'text',
|
|
307
|
+
text: expect.stringContaining('pid'),
|
|
308
|
+
}]);
|
|
278
309
|
});
|
|
279
310
|
});
|