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,200 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { accessSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
vi.mock('node:fs', () => ({
|
|
5
|
-
accessSync: vi.fn(),
|
|
6
|
-
constants: { X_OK: 1 },
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
const mockAccessSync = vi.mocked(accessSync);
|
|
10
|
-
|
|
11
|
-
describe('cli-utils doctor status', () => {
|
|
12
|
-
const originalEnv = process.env;
|
|
13
|
-
const originalPlatform = process.platform;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.resetModules();
|
|
17
|
-
mockAccessSync.mockReset();
|
|
18
|
-
process.env = { ...originalEnv };
|
|
19
|
-
delete process.env.CLAUDE_CLI_NAME;
|
|
20
|
-
delete process.env.CODEX_CLI_NAME;
|
|
21
|
-
delete process.env.GEMINI_CLI_NAME;
|
|
22
|
-
delete process.env.FORGE_CLI_NAME;
|
|
23
|
-
delete process.env.OPENCODE_CLI_NAME;
|
|
24
|
-
process.env.PATH = '/mock/bin:/usr/bin';
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
process.env = originalEnv;
|
|
29
|
-
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('marks PATH binaries available when they are executable', async () => {
|
|
33
|
-
mockAccessSync.mockImplementation((filePath) => {
|
|
34
|
-
if (filePath === '/mock/bin/claude') {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
throw new Error('not executable');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const { getCliDoctorStatus } = await import('../cli-utils.js');
|
|
41
|
-
const status = getCliDoctorStatus();
|
|
42
|
-
|
|
43
|
-
expect(status.claude).toEqual({
|
|
44
|
-
configuredCommand: 'claude',
|
|
45
|
-
resolvedPath: '/mock/bin/claude',
|
|
46
|
-
available: true,
|
|
47
|
-
lookup: 'path',
|
|
48
|
-
});
|
|
49
|
-
expect(status.forge).toEqual({
|
|
50
|
-
configuredCommand: 'forge',
|
|
51
|
-
resolvedPath: null,
|
|
52
|
-
available: false,
|
|
53
|
-
lookup: 'path',
|
|
54
|
-
});
|
|
55
|
-
expect(status.opencode).toEqual({
|
|
56
|
-
configuredCommand: 'opencode',
|
|
57
|
-
resolvedPath: null,
|
|
58
|
-
available: false,
|
|
59
|
-
lookup: 'path',
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('does not mark non-executable PATH entries as available', async () => {
|
|
64
|
-
mockAccessSync.mockImplementation(() => {
|
|
65
|
-
throw new Error('not executable');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const { getCliDoctorStatus } = await import('../cli-utils.js');
|
|
69
|
-
const status = getCliDoctorStatus();
|
|
70
|
-
|
|
71
|
-
expect(status.claude).toEqual({
|
|
72
|
-
configuredCommand: 'claude',
|
|
73
|
-
resolvedPath: null,
|
|
74
|
-
available: false,
|
|
75
|
-
lookup: 'path',
|
|
76
|
-
});
|
|
77
|
-
expect(status.forge).toEqual({
|
|
78
|
-
configuredCommand: 'forge',
|
|
79
|
-
resolvedPath: null,
|
|
80
|
-
available: false,
|
|
81
|
-
lookup: 'path',
|
|
82
|
-
});
|
|
83
|
-
expect(status.opencode).toEqual({
|
|
84
|
-
configuredCommand: 'opencode',
|
|
85
|
-
resolvedPath: null,
|
|
86
|
-
available: false,
|
|
87
|
-
lookup: 'path',
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('reports invalid relative env paths as doctor errors', async () => {
|
|
92
|
-
process.env.CLAUDE_CLI_NAME = './relative/claude';
|
|
93
|
-
|
|
94
|
-
const { getCliDoctorStatus } = await import('../cli-utils.js');
|
|
95
|
-
const status = getCliDoctorStatus();
|
|
96
|
-
|
|
97
|
-
expect(status.claude.available).toBe(false);
|
|
98
|
-
expect(status.claude.lookup).toBe('env');
|
|
99
|
-
expect(status.claude.error).toContain('Invalid CLAUDE_CLI_NAME');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('reports missing absolute env paths as unavailable', async () => {
|
|
103
|
-
process.env.CLAUDE_CLI_NAME = '/missing/claude';
|
|
104
|
-
mockAccessSync.mockImplementation(() => {
|
|
105
|
-
throw new Error('missing');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const { getCliDoctorStatus } = await import('../cli-utils.js');
|
|
109
|
-
const status = getCliDoctorStatus();
|
|
110
|
-
|
|
111
|
-
expect(status.claude).toEqual({
|
|
112
|
-
configuredCommand: '/missing/claude',
|
|
113
|
-
resolvedPath: '/missing/claude',
|
|
114
|
-
available: false,
|
|
115
|
-
lookup: 'env',
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('falls back cleanly when PATH is empty', async () => {
|
|
120
|
-
process.env.PATH = '';
|
|
121
|
-
mockAccessSync.mockImplementation(() => {
|
|
122
|
-
throw new Error('missing');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const { getCliDoctorStatus } = await import('../cli-utils.js');
|
|
126
|
-
const status = getCliDoctorStatus();
|
|
127
|
-
|
|
128
|
-
expect(status.codex).toEqual({
|
|
129
|
-
configuredCommand: 'codex',
|
|
130
|
-
resolvedPath: null,
|
|
131
|
-
available: false,
|
|
132
|
-
lookup: 'path',
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('supports Windows commands that already include an executable suffix', async () => {
|
|
137
|
-
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
138
|
-
process.env.PATHEXT = '.EXE;.CMD';
|
|
139
|
-
process.env.CLAUDE_CLI_NAME = 'claude.cmd';
|
|
140
|
-
process.env.PATH = '/mock/bin';
|
|
141
|
-
mockAccessSync.mockImplementation((filePath) => {
|
|
142
|
-
if (filePath === '/mock/bin/claude.cmd') {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
throw new Error('not executable');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const { getCliDoctorStatus } = await import('../cli-utils.js');
|
|
149
|
-
const status = getCliDoctorStatus();
|
|
150
|
-
|
|
151
|
-
expect(status.claude).toEqual({
|
|
152
|
-
configuredCommand: 'claude.cmd',
|
|
153
|
-
resolvedPath: '/mock/bin/claude.cmd',
|
|
154
|
-
available: true,
|
|
155
|
-
lookup: 'env',
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('supports forge lookup via FORGE_CLI_NAME', async () => {
|
|
160
|
-
process.env.FORGE_CLI_NAME = 'forge-custom';
|
|
161
|
-
mockAccessSync.mockImplementation((filePath) => {
|
|
162
|
-
if (filePath === '/mock/bin/forge-custom') {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
throw new Error('not executable');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const { getCliDoctorStatus, findForgeCli } = await import('../cli-utils.js');
|
|
169
|
-
const status = getCliDoctorStatus();
|
|
170
|
-
|
|
171
|
-
expect(status.forge).toEqual({
|
|
172
|
-
configuredCommand: 'forge-custom',
|
|
173
|
-
resolvedPath: '/mock/bin/forge-custom',
|
|
174
|
-
available: true,
|
|
175
|
-
lookup: 'env',
|
|
176
|
-
});
|
|
177
|
-
expect(findForgeCli()).toBe('forge-custom');
|
|
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
|
-
});
|
|
200
|
-
});
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { createTestClient, MCPTestClient } from './utils/mcp-client.js';
|
|
6
|
-
import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
|
|
7
|
-
import { createOpenCodeMock } from './utils/opencode-mock.js';
|
|
8
|
-
|
|
9
|
-
describe('Claude Code MCP E2E Tests', () => {
|
|
10
|
-
let client: MCPTestClient;
|
|
11
|
-
let testDir: string;
|
|
12
|
-
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
// Ensure mock exists
|
|
15
|
-
await getSharedMock();
|
|
16
|
-
|
|
17
|
-
// Create a temporary directory for test files
|
|
18
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
|
|
19
|
-
|
|
20
|
-
client = createTestClient();
|
|
21
|
-
|
|
22
|
-
await client.connect();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(async () => {
|
|
26
|
-
// Disconnect client
|
|
27
|
-
await client.disconnect();
|
|
28
|
-
|
|
29
|
-
// Clean up test directory
|
|
30
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterAll(async () => {
|
|
34
|
-
// Only cleanup mock at the very end
|
|
35
|
-
await cleanupSharedMock();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('Tool Registration', () => {
|
|
39
|
-
it('should register run tool', async () => {
|
|
40
|
-
const tools = await client.listTools();
|
|
41
|
-
|
|
42
|
-
expect(tools).toHaveLength(7);
|
|
43
|
-
const claudeCodeTool = tools.find((t: any) => t.name === 'run');
|
|
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');
|
|
48
|
-
|
|
49
|
-
// Verify other tools exist
|
|
50
|
-
expect(tools.some((t: any) => t.name === 'list_processes')).toBe(true);
|
|
51
|
-
expect(tools.some((t: any) => t.name === 'get_result')).toBe(true);
|
|
52
|
-
expect(tools.some((t: any) => t.name === 'peek')).toBe(true);
|
|
53
|
-
expect(tools.some((t: any) => t.name === 'kill_process')).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('Basic Operations', () => {
|
|
58
|
-
it('should execute a simple prompt', async () => {
|
|
59
|
-
const response = await client.callTool('run', {
|
|
60
|
-
prompt: 'create a file called test.txt with content "Hello World"',
|
|
61
|
-
workFolder: testDir,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(response).toEqual([{
|
|
65
|
-
type: 'text',
|
|
66
|
-
text: expect.stringContaining('successfully'),
|
|
67
|
-
}]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should handle process management correctly', async () => {
|
|
71
|
-
// run now returns a PID immediately
|
|
72
|
-
const response = await client.callTool('run', {
|
|
73
|
-
prompt: 'error',
|
|
74
|
-
workFolder: testDir,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(response).toEqual([{
|
|
78
|
-
type: 'text',
|
|
79
|
-
text: expect.stringContaining('pid'),
|
|
80
|
-
}]);
|
|
81
|
-
|
|
82
|
-
// Extract PID from response
|
|
83
|
-
const responseText = response[0].text;
|
|
84
|
-
const pidMatch = responseText.match(/"pid":\s*(\d+)/);
|
|
85
|
-
expect(pidMatch).toBeTruthy();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should reject missing workFolder', async () => {
|
|
89
|
-
await expect(
|
|
90
|
-
client.callTool('run', {
|
|
91
|
-
prompt: 'List files in current directory',
|
|
92
|
-
})
|
|
93
|
-
).rejects.toThrow(/workFolder/i);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('Working Directory Handling', () => {
|
|
98
|
-
it('should respect custom working directory', async () => {
|
|
99
|
-
const response = await client.callTool('run', {
|
|
100
|
-
prompt: 'Show current working directory',
|
|
101
|
-
workFolder: testDir,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
expect(response).toBeTruthy();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should reject non-existent working directory', async () => {
|
|
108
|
-
const nonExistentDir = join(testDir, 'non-existent');
|
|
109
|
-
|
|
110
|
-
await expect(
|
|
111
|
-
client.callTool('run', {
|
|
112
|
-
prompt: 'Test prompt',
|
|
113
|
-
workFolder: nonExistentDir,
|
|
114
|
-
})
|
|
115
|
-
).rejects.toThrow(/does not exist/i);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('Timeout Handling', () => {
|
|
120
|
-
it('should respect timeout settings', async () => {
|
|
121
|
-
// This would require modifying the mock to simulate a long-running command
|
|
122
|
-
// Since we're testing locally, we'll skip the actual timeout test
|
|
123
|
-
expect(true).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('Model Alias Handling', () => {
|
|
128
|
-
it('should resolve haiku alias when calling run', async () => {
|
|
129
|
-
const response = await client.callTool('run', {
|
|
130
|
-
prompt: 'Test with haiku model',
|
|
131
|
-
workFolder: testDir,
|
|
132
|
-
model: 'haiku'
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(response).toEqual([{
|
|
136
|
-
type: 'text',
|
|
137
|
-
text: expect.stringContaining('pid'),
|
|
138
|
-
}]);
|
|
139
|
-
|
|
140
|
-
// Extract PID from response
|
|
141
|
-
const responseText = response[0].text;
|
|
142
|
-
const pidMatch = responseText.match(/"pid":\s*(\d+)/);
|
|
143
|
-
expect(pidMatch).toBeTruthy();
|
|
144
|
-
|
|
145
|
-
// Get the PID and check the process using get_result
|
|
146
|
-
const pid = parseInt(pidMatch![1]);
|
|
147
|
-
const result = await client.callTool('get_result', { pid });
|
|
148
|
-
const resultText = result[0].text;
|
|
149
|
-
const processData = JSON.parse(resultText);
|
|
150
|
-
|
|
151
|
-
// Verify that the model was set correctly
|
|
152
|
-
expect(processData.model).toBe('haiku');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should pass non-alias model names unchanged', async () => {
|
|
156
|
-
const response = await client.callTool('run', {
|
|
157
|
-
prompt: 'Test with sonnet model',
|
|
158
|
-
workFolder: testDir,
|
|
159
|
-
model: 'sonnet'
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
expect(response).toEqual([{
|
|
163
|
-
type: 'text',
|
|
164
|
-
text: expect.stringContaining('pid'),
|
|
165
|
-
}]);
|
|
166
|
-
|
|
167
|
-
// Extract PID
|
|
168
|
-
const responseText = response[0].text;
|
|
169
|
-
const pidMatch = responseText.match(/"pid":\s*(\d+)/);
|
|
170
|
-
const pid = parseInt(pidMatch![1]);
|
|
171
|
-
|
|
172
|
-
// Check the process using get_result
|
|
173
|
-
const result = await client.callTool('get_result', { pid });
|
|
174
|
-
const resultText = result[0].text;
|
|
175
|
-
const processData = JSON.parse(resultText);
|
|
176
|
-
|
|
177
|
-
// The model should be unchanged
|
|
178
|
-
expect(processData.model).toBe('sonnet');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('should work without specifying a model', async () => {
|
|
182
|
-
const response = await client.callTool('run', {
|
|
183
|
-
prompt: 'Test without model parameter',
|
|
184
|
-
workFolder: testDir
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(response).toEqual([{
|
|
188
|
-
type: 'text',
|
|
189
|
-
text: expect.stringContaining('pid'),
|
|
190
|
-
}]);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
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
|
-
|
|
266
|
-
describe('Debug Mode', () => {
|
|
267
|
-
it('should log debug information when enabled', async () => {
|
|
268
|
-
// Debug logs go to stderr, which we capture in the client
|
|
269
|
-
const response = await client.callTool('run', {
|
|
270
|
-
prompt: 'Debug test prompt',
|
|
271
|
-
workFolder: testDir,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
expect(response).toBeTruthy();
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe('Integration Tests (Local Only)', () => {
|
|
280
|
-
let client: MCPTestClient;
|
|
281
|
-
let testDir: string;
|
|
282
|
-
|
|
283
|
-
beforeEach(async () => {
|
|
284
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-integration-'));
|
|
285
|
-
|
|
286
|
-
// Initialize client without mocks for real Claude testing
|
|
287
|
-
client = createTestClient({ claudeCliName: '' });
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
afterEach(async () => {
|
|
291
|
-
if (client) {
|
|
292
|
-
await client.disconnect();
|
|
293
|
-
}
|
|
294
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// This smoke test only verifies that a real Claude CLI can be invoked.
|
|
298
|
-
it.skip('should invoke the real Claude CLI', async () => {
|
|
299
|
-
await client.connect();
|
|
300
|
-
|
|
301
|
-
const response = await client.callTool('run', {
|
|
302
|
-
prompt: 'Reply with hi',
|
|
303
|
-
workFolder: testDir,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
expect(response).toEqual([{
|
|
307
|
-
type: 'text',
|
|
308
|
-
text: expect.stringContaining('pid'),
|
|
309
|
-
}]);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { createTestClient, MCPTestClient } from './utils/mcp-client.js';
|
|
6
|
-
import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
|
|
7
|
-
|
|
8
|
-
describe('Claude Code Edge Cases', () => {
|
|
9
|
-
let client: MCPTestClient;
|
|
10
|
-
let testDir: string;
|
|
11
|
-
|
|
12
|
-
beforeEach(async () => {
|
|
13
|
-
// Ensure mock exists
|
|
14
|
-
await getSharedMock();
|
|
15
|
-
|
|
16
|
-
// Create test directory
|
|
17
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-edge-'));
|
|
18
|
-
|
|
19
|
-
client = createTestClient();
|
|
20
|
-
await client.connect();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await client.disconnect();
|
|
25
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterAll(async () => {
|
|
29
|
-
// Cleanup mock only at the end
|
|
30
|
-
await cleanupSharedMock();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('Input Validation', () => {
|
|
34
|
-
it('should reject missing prompt', async () => {
|
|
35
|
-
await expect(
|
|
36
|
-
client.callTool('run', {
|
|
37
|
-
workFolder: testDir,
|
|
38
|
-
})
|
|
39
|
-
).rejects.toThrow(/prompt/i);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should reject invalid prompt type', async () => {
|
|
43
|
-
await expect(
|
|
44
|
-
client.callTool('run', {
|
|
45
|
-
prompt: 123, // Should be string
|
|
46
|
-
workFolder: testDir,
|
|
47
|
-
})
|
|
48
|
-
).rejects.toThrow();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should reject invalid workFolder type', async () => {
|
|
52
|
-
await expect(
|
|
53
|
-
client.callTool('run', {
|
|
54
|
-
prompt: 'Test prompt',
|
|
55
|
-
workFolder: 123, // Should be string
|
|
56
|
-
})
|
|
57
|
-
).rejects.toThrow(/workFolder/i);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should reject empty prompt', async () => {
|
|
61
|
-
await expect(
|
|
62
|
-
client.callTool('run', {
|
|
63
|
-
prompt: '',
|
|
64
|
-
workFolder: testDir,
|
|
65
|
-
})
|
|
66
|
-
).rejects.toThrow(/prompt/i);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('Special Characters', () => {
|
|
71
|
-
it.skip('should handle prompts with quotes', async () => {
|
|
72
|
-
// Skipping: This test fails in CI when mock is not found at expected path
|
|
73
|
-
const response = await client.callTool('run', {
|
|
74
|
-
prompt: 'Create a file with content "Hello \\"World\\""',
|
|
75
|
-
workFolder: testDir,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
expect(response).toBeTruthy();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should handle prompts with newlines', async () => {
|
|
82
|
-
const response = await client.callTool('run', {
|
|
83
|
-
prompt: 'Create a file with content:\\nLine 1\\nLine 2',
|
|
84
|
-
workFolder: testDir,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(response).toBeTruthy();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should handle prompts with shell special characters', async () => {
|
|
91
|
-
const response = await client.callTool('run', {
|
|
92
|
-
prompt: 'Create a file named test$file.txt',
|
|
93
|
-
workFolder: testDir,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(response).toBeTruthy();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('Error Recovery', () => {
|
|
101
|
-
it('should handle Claude CLI not found gracefully', async () => {
|
|
102
|
-
// Create a client with a different binary name that doesn't exist
|
|
103
|
-
const errorClient = createTestClient({ claudeCliName: 'non-existent-claude' });
|
|
104
|
-
await errorClient.connect();
|
|
105
|
-
|
|
106
|
-
await expect(
|
|
107
|
-
errorClient.callTool('run', {
|
|
108
|
-
prompt: 'Test prompt',
|
|
109
|
-
workFolder: testDir,
|
|
110
|
-
})
|
|
111
|
-
).rejects.toThrow();
|
|
112
|
-
|
|
113
|
-
await errorClient.disconnect();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should handle permission denied errors', async () => {
|
|
117
|
-
const restrictedDir = '/root/restricted';
|
|
118
|
-
|
|
119
|
-
// Non-existent directories now throw an error
|
|
120
|
-
await expect(
|
|
121
|
-
client.callTool('run', {
|
|
122
|
-
prompt: 'Test prompt',
|
|
123
|
-
workFolder: restrictedDir,
|
|
124
|
-
})
|
|
125
|
-
).rejects.toThrow(/does not exist/i);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('Concurrent Requests', () => {
|
|
130
|
-
it('should handle multiple simultaneous requests', async () => {
|
|
131
|
-
const promises = Array(5).fill(null).map((_, i) =>
|
|
132
|
-
client.callTool('run', {
|
|
133
|
-
prompt: `Create file test${i}.txt`,
|
|
134
|
-
workFolder: testDir,
|
|
135
|
-
})
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const results = await Promise.allSettled(promises);
|
|
139
|
-
const successful = results.filter(r => r.status === 'fulfilled');
|
|
140
|
-
|
|
141
|
-
const failures = results
|
|
142
|
-
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
|
|
143
|
-
.map((r) => r.reason?.message ?? String(r.reason));
|
|
144
|
-
|
|
145
|
-
expect(successful.length, `Concurrent run failures: ${failures.join(' | ')}`).toBeGreaterThan(0);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('Large Prompts', () => {
|
|
150
|
-
it('should handle very long prompts', async () => {
|
|
151
|
-
const longPrompt = 'Create a file with content: ' + 'x'.repeat(10000);
|
|
152
|
-
|
|
153
|
-
const response = await client.callTool('run', {
|
|
154
|
-
prompt: longPrompt,
|
|
155
|
-
workFolder: testDir,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
expect(response).toBeTruthy();
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('Path Traversal', () => {
|
|
163
|
-
it('should prevent path traversal attacks', async () => {
|
|
164
|
-
const maliciousPath = join(testDir, '..', '..', 'etc', 'passwd');
|
|
165
|
-
|
|
166
|
-
// Server resolves paths and checks existence
|
|
167
|
-
// The path /etc/passwd may exist but be a file, not a directory
|
|
168
|
-
await expect(
|
|
169
|
-
client.callTool('run', {
|
|
170
|
-
prompt: 'Read file',
|
|
171
|
-
workFolder: maliciousPath,
|
|
172
|
-
})
|
|
173
|
-
).rejects.toThrow(/(does not exist|ENOTDIR)/i);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|