ai-cli-mcp 2.19.0 → 2.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +34 -8
  3. package/README.md +41 -8
  4. package/dist/app/cli.js +1 -0
  5. package/dist/app/mcp.js +64 -12
  6. package/dist/cli-builder.js +13 -6
  7. package/dist/cli-process-service.js +76 -91
  8. package/dist/cli-utils.js +6 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/model-catalog.js +3 -2
  11. package/dist/parsers.js +8 -2
  12. package/package.json +27 -3
  13. package/server.json +3 -3
  14. package/.gemini/settings.json +0 -11
  15. package/.github/dependabot.yml +0 -28
  16. package/.github/pull_request_template.md +0 -28
  17. package/.github/workflows/ci.yml +0 -34
  18. package/.github/workflows/dependency-review.yml +0 -22
  19. package/.github/workflows/publish.yml +0 -89
  20. package/.github/workflows/test.yml +0 -20
  21. package/.github/workflows/watch-session-prs.yml +0 -276
  22. package/.husky/pre-commit +0 -1
  23. package/.mcp.json +0 -11
  24. package/.releaserc.json +0 -18
  25. package/.vscode/settings.json +0 -3
  26. package/CONTRIBUTING.md +0 -81
  27. package/dist/__tests__/app-cli.test.js +0 -392
  28. package/dist/__tests__/cli-bin-smoke.test.js +0 -101
  29. package/dist/__tests__/cli-builder.test.js +0 -442
  30. package/dist/__tests__/cli-process-service.test.js +0 -655
  31. package/dist/__tests__/cli-utils.test.js +0 -171
  32. package/dist/__tests__/e2e.test.js +0 -256
  33. package/dist/__tests__/edge-cases.test.js +0 -130
  34. package/dist/__tests__/error-cases.test.js +0 -292
  35. package/dist/__tests__/mcp-contract.test.js +0 -636
  36. package/dist/__tests__/mocks.js +0 -32
  37. package/dist/__tests__/model-alias.test.js +0 -36
  38. package/dist/__tests__/parsers.test.js +0 -646
  39. package/dist/__tests__/peek.test.js +0 -36
  40. package/dist/__tests__/process-management.test.js +0 -949
  41. package/dist/__tests__/server.test.js +0 -809
  42. package/dist/__tests__/setup.js +0 -11
  43. package/dist/__tests__/utils/claude-mock.js +0 -80
  44. package/dist/__tests__/utils/mcp-client.js +0 -121
  45. package/dist/__tests__/utils/opencode-mock.js +0 -91
  46. package/dist/__tests__/utils/persistent-mock.js +0 -28
  47. package/dist/__tests__/utils/test-helpers.js +0 -11
  48. package/dist/__tests__/validation.test.js +0 -308
  49. package/dist/__tests__/version-print.test.js +0 -65
  50. package/dist/__tests__/wait.test.js +0 -260
  51. package/docs/RELEASE_CHECKLIST.md +0 -65
  52. package/docs/cli-architecture.md +0 -275
  53. package/docs/concept.md +0 -154
  54. package/docs/development.md +0 -156
  55. package/docs/e2e-testing.md +0 -148
  56. package/docs/prd.md +0 -146
  57. package/docs/session-stacking.md +0 -67
  58. package/src/__tests__/app-cli.test.ts +0 -495
  59. package/src/__tests__/cli-bin-smoke.test.ts +0 -136
  60. package/src/__tests__/cli-builder.test.ts +0 -549
  61. package/src/__tests__/cli-process-service.test.ts +0 -759
  62. package/src/__tests__/cli-utils.test.ts +0 -200
  63. package/src/__tests__/e2e.test.ts +0 -311
  64. package/src/__tests__/edge-cases.test.ts +0 -176
  65. package/src/__tests__/error-cases.test.ts +0 -370
  66. package/src/__tests__/mcp-contract.test.ts +0 -755
  67. package/src/__tests__/mocks.ts +0 -35
  68. package/src/__tests__/model-alias.test.ts +0 -44
  69. package/src/__tests__/parsers.test.ts +0 -730
  70. package/src/__tests__/peek.test.ts +0 -44
  71. package/src/__tests__/process-management.test.ts +0 -1129
  72. package/src/__tests__/server.test.ts +0 -1020
  73. package/src/__tests__/setup.ts +0 -13
  74. package/src/__tests__/utils/claude-mock.ts +0 -87
  75. package/src/__tests__/utils/mcp-client.ts +0 -159
  76. package/src/__tests__/utils/opencode-mock.ts +0 -108
  77. package/src/__tests__/utils/persistent-mock.ts +0 -33
  78. package/src/__tests__/utils/test-helpers.ts +0 -13
  79. package/src/__tests__/validation.test.ts +0 -369
  80. package/src/__tests__/version-print.test.ts +0 -81
  81. package/src/__tests__/wait.test.ts +0 -302
  82. package/src/app/cli.ts +0 -424
  83. package/src/app/mcp.ts +0 -466
  84. package/src/bin/ai-cli-mcp.ts +0 -7
  85. package/src/bin/ai-cli.ts +0 -11
  86. package/src/cli-builder.ts +0 -274
  87. package/src/cli-parse.ts +0 -105
  88. package/src/cli-process-service.ts +0 -709
  89. package/src/cli-utils.ts +0 -258
  90. package/src/cli.ts +0 -124
  91. package/src/model-catalog.ts +0 -87
  92. package/src/parsers.ts +0 -965
  93. package/src/peek.ts +0 -95
  94. package/src/process-result.ts +0 -88
  95. package/src/process-service.ts +0 -368
  96. package/src/server.ts +0 -10
  97. package/tsconfig.json +0 -16
  98. package/vitest.config.e2e.ts +0 -27
  99. package/vitest.config.ts +0 -22
  100. package/vitest.config.unit.ts +0 -28
@@ -1,11 +0,0 @@
1
- // Global test setup
2
- import { beforeAll, afterAll } from 'vitest';
3
- import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
4
- beforeAll(async () => {
5
- console.error('[TEST SETUP] Creating shared mock for all tests...');
6
- await getSharedMock();
7
- });
8
- afterAll(async () => {
9
- console.error('[TEST SETUP] Cleaning up shared mock...');
10
- await cleanupSharedMock();
11
- });
@@ -1,80 +0,0 @@
1
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- /**
4
- * Mock Claude CLI for testing
5
- * This creates a fake Claude CLI that can be used during testing
6
- */
7
- export class ClaudeMock {
8
- mockPath;
9
- responses = new Map();
10
- constructor(binaryName = 'claude') {
11
- // Always use /tmp directory for mocks in tests
12
- this.mockPath = join('/tmp', 'claude-code-test-mock', binaryName);
13
- }
14
- /**
15
- * Setup the mock Claude CLI
16
- */
17
- async setup() {
18
- const dir = dirname(this.mockPath);
19
- if (!existsSync(dir)) {
20
- mkdirSync(dir, { recursive: true });
21
- }
22
- // Create a simple bash script that echoes responses
23
- const mockScript = `#!/bin/bash
24
- # Mock Claude CLI for testing
25
-
26
- # Extract the prompt from arguments
27
- prompt=""
28
- verbose=false
29
- while [[ $# -gt 0 ]]; do
30
- case $1 in
31
- -p|--prompt)
32
- prompt="$2"
33
- shift 2
34
- ;;
35
- --verbose)
36
- verbose=true
37
- shift
38
- ;;
39
- --yes|-y|--dangerously-skip-permissions)
40
- shift
41
- ;;
42
- *)
43
- shift
44
- ;;
45
- esac
46
- done
47
-
48
- # Mock responses based on prompt
49
- if [[ "$prompt" == *"create"* ]]; then
50
- echo "Created file successfully"
51
- elif [[ "$prompt" == *"Create"* ]]; then
52
- echo "Created file successfully"
53
- elif [[ "$prompt" == *"git"* ]] && [[ "$prompt" == *"commit"* ]]; then
54
- echo "Committed changes successfully"
55
- elif [[ "$prompt" == *"error"* ]]; then
56
- echo "Error: Mock error response" >&2
57
- exit 1
58
- else
59
- echo "Command executed successfully"
60
- fi
61
- `;
62
- writeFileSync(this.mockPath, mockScript);
63
- // Make executable
64
- const { chmod } = await import('node:fs/promises');
65
- await chmod(this.mockPath, 0o755);
66
- }
67
- /**
68
- * Cleanup the mock Claude CLI
69
- */
70
- async cleanup() {
71
- const { rm } = await import('node:fs/promises');
72
- await rm(this.mockPath, { force: true });
73
- }
74
- /**
75
- * Add a mock response for a specific prompt pattern
76
- */
77
- addResponse(pattern, response) {
78
- this.responses.set(pattern, response);
79
- }
80
- }
@@ -1,121 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { EventEmitter } from 'node:events';
3
- /**
4
- * Mock MCP client for testing the server
5
- */
6
- export class MCPTestClient extends EventEmitter {
7
- serverPath;
8
- env;
9
- server = null;
10
- requestId = 0;
11
- pendingRequests = new Map();
12
- buffer = '';
13
- constructor(serverPath, env = {}) {
14
- super();
15
- this.serverPath = serverPath;
16
- this.env = env;
17
- }
18
- async connect() {
19
- return new Promise((resolve, reject) => {
20
- this.server = spawn('node', [this.serverPath], {
21
- env: { ...process.env, ...this.env },
22
- stdio: ['pipe', 'pipe', 'pipe'],
23
- });
24
- this.server.stdout?.on('data', (data) => {
25
- this.handleData(data.toString());
26
- });
27
- this.server.stderr?.on('data', (data) => {
28
- console.error('Server stderr:', data.toString());
29
- });
30
- this.server.on('error', (error) => {
31
- reject(error);
32
- });
33
- this.server.on('spawn', () => {
34
- resolve();
35
- });
36
- });
37
- }
38
- async disconnect() {
39
- if (this.server) {
40
- this.server.kill();
41
- await new Promise((resolve) => {
42
- this.server.on('exit', resolve);
43
- });
44
- this.server = null;
45
- }
46
- }
47
- handleData(data) {
48
- this.buffer += data;
49
- const lines = this.buffer.split('\n');
50
- this.buffer = lines.pop() || '';
51
- for (const line of lines) {
52
- if (!line.trim())
53
- continue;
54
- try {
55
- const response = JSON.parse(line);
56
- if (response.id && this.pendingRequests.has(response.id)) {
57
- const pending = this.pendingRequests.get(response.id);
58
- this.pendingRequests.delete(response.id);
59
- pending.resolve(response);
60
- }
61
- else {
62
- this.emit('notification', response);
63
- }
64
- }
65
- catch (error) {
66
- console.error('Failed to parse response:', line, error);
67
- }
68
- }
69
- }
70
- async sendRequest(method, params) {
71
- const id = ++this.requestId;
72
- const request = {
73
- jsonrpc: '2.0',
74
- method,
75
- params,
76
- id,
77
- };
78
- return new Promise((resolve, reject) => {
79
- this.pendingRequests.set(id, { resolve, reject });
80
- this.server?.stdin?.write(JSON.stringify(request) + '\n');
81
- // Timeout after 30 seconds
82
- setTimeout(() => {
83
- if (this.pendingRequests.has(id)) {
84
- this.pendingRequests.delete(id);
85
- reject(new Error(`Request ${id} timed out`));
86
- }
87
- }, 30000);
88
- });
89
- }
90
- async callTool(name, args) {
91
- const response = await this.sendRequest('tools/call', {
92
- name,
93
- arguments: args,
94
- });
95
- if (response.error) {
96
- throw new Error(`Tool call failed: ${response.error.message}`);
97
- }
98
- return response.result?.content;
99
- }
100
- async listTools() {
101
- const response = await this.sendRequest('tools/list');
102
- return response.result?.tools || [];
103
- }
104
- }
105
- /**
106
- * Default server path
107
- */
108
- const DEFAULT_SERVER_PATH = 'dist/server.js';
109
- /**
110
- * Create a test client with standard configuration
111
- * Automatically unsets VITEST env so the server actually starts
112
- */
113
- export function createTestClient(options = {}) {
114
- const { serverPath = DEFAULT_SERVER_PATH, claudeCliName = process.env.TEST_CLAUDE_CLI_NAME || '/tmp/claude-code-test-mock/claudeMocked', debug = true, env = {}, } = options;
115
- return new MCPTestClient(serverPath, {
116
- VITEST: '', // Unset so server starts
117
- MCP_CLAUDE_DEBUG: debug ? 'true' : '',
118
- CLAUDE_CLI_NAME: claudeCliName,
119
- ...env,
120
- });
121
- }
@@ -1,91 +0,0 @@
1
- import { chmodSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- export function createOpenCodeMock(dir, options = {}) {
4
- const scriptPath = join(dir, 'mock-opencode');
5
- const defaultSessionId = options.defaultSessionId || 'ses-opencode-default';
6
- const argsLogPath = options.argsLogPath;
7
- const argsLogSection = argsLogPath
8
- ? `printf '%s\n' "$*" >> "${argsLogPath}"\n`
9
- : '';
10
- writeFileSync(scriptPath, `#!/bin/bash
11
- set -euo pipefail
12
-
13
- prompt=""
14
- session_id=""
15
- session_provided=0
16
- model=""
17
- work_dir=""
18
-
19
- ${argsLogSection}if [[ "\${1:-}" == "run" ]]; then
20
- shift
21
- fi
22
-
23
- while [[ $# -gt 0 ]]; do
24
- case "$1" in
25
- --format)
26
- shift 2
27
- ;;
28
- --dir)
29
- work_dir="$2"
30
- shift 2
31
- ;;
32
- --session)
33
- session_id="$2"
34
- session_provided=1
35
- shift 2
36
- ;;
37
- --model)
38
- model="$2"
39
- shift 2
40
- ;;
41
- *)
42
- prompt="$1"
43
- shift
44
- ;;
45
- esac
46
- done
47
-
48
- if [[ -z "$session_id" ]]; then
49
- session_id="${defaultSessionId}"
50
- fi
51
-
52
- if [[ "$prompt" == *"sleep"* ]]; then
53
- sleep 5
54
- fi
55
-
56
- if [[ "$prompt" == *"fail"* ]]; then
57
- printf '{"type":"step_start","sessionID":"%s"}\n' "$session_id"
58
- printf '{"type":"text","sessionID":"%s","part":{"type":"text","text":"Partial failure output"}}\n' "$session_id"
59
- printf '{"type":"step_finish","sessionID":"%s","part":{"type":"step-finish","tokens":{"total":42},"cost":0}}\n' "$session_id"
60
- printf 'OpenCode failed for %s in %s\n' "$model" "$work_dir" >&2
61
- exit 7
62
- fi
63
-
64
- if [[ "$prompt" == *"multi-step"* ]]; then
65
- printf '{"type":"step_start","sessionID":"%s"}\n' "$session_id"
66
- printf '{"type":"text","sessionID":"%s","part":{"type":"text","text":"First step"}}\n' "$session_id"
67
- printf '{"type":"step_finish","sessionID":"%s","part":{"type":"step-finish","tokens":{"total":11},"cost":0}}\n' "$session_id"
68
- printf '{"type":"step_start","sessionID":"%s"}\n' "$session_id"
69
- printf '{"type":"text","sessionID":"%s","part":{"type":"text","text":"Second step"}}\n' "$session_id"
70
- printf '{"type":"step_finish","sessionID":"%s","part":{"type":"step-finish","tokens":{"total":22},"cost":1}}\n' "$session_id"
71
- exit 0
72
- fi
73
-
74
- message_prefix="Initial"
75
- if [[ $session_provided -eq 1 ]]; then
76
- message_prefix="Resumed"
77
- fi
78
- if [[ -n "$model" ]]; then
79
- message_prefix="Model $model"
80
- if [[ $session_provided -eq 1 ]]; then
81
- message_prefix="Resumed model $model"
82
- fi
83
- fi
84
-
85
- printf '{"type":"step_start","sessionID":"%s"}\n' "$session_id"
86
- printf '{"type":"text","sessionID":"%s","part":{"type":"text","text":"%s: %s"}}\n' "$session_id" "$message_prefix" "$prompt"
87
- printf '{"type":"step_finish","sessionID":"%s","part":{"type":"step-finish","tokens":{"total":11833},"cost":0}}\n' "$session_id"
88
- `, 'utf8');
89
- chmodSync(scriptPath, 0o755);
90
- return { scriptPath, argsLogPath };
91
- }
@@ -1,28 +0,0 @@
1
- import { ClaudeMock } from './claude-mock.js';
2
- import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- let sharedMock = null;
5
- const workerId = process.env.VITEST_WORKER_ID || process.env.VITEST_POOL_ID || process.pid.toString();
6
- const mockName = `claudeMocked-${workerId}`;
7
- const mockPath = join('/tmp', 'claude-code-test-mock', mockName);
8
- export async function getSharedMock() {
9
- if (!sharedMock) {
10
- sharedMock = new ClaudeMock(mockName);
11
- }
12
- // Always ensure mock exists
13
- if (!existsSync(mockPath)) {
14
- console.error(`[DEBUG] Mock not found at ${mockPath}, creating it...`);
15
- await sharedMock.setup();
16
- }
17
- else {
18
- console.error(`[DEBUG] Mock already exists at ${mockPath}`);
19
- }
20
- process.env.TEST_CLAUDE_CLI_NAME = mockPath;
21
- return sharedMock;
22
- }
23
- export async function cleanupSharedMock() {
24
- if (sharedMock) {
25
- await sharedMock.cleanup();
26
- sharedMock = null;
27
- }
28
- }
@@ -1,11 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- export function verifyMockExists(binaryName) {
4
- const mockPath = join('/tmp', 'claude-code-test-mock', binaryName);
5
- return existsSync(mockPath);
6
- }
7
- export async function ensureMockExists(mock) {
8
- if (!verifyMockExists('claudeMocked')) {
9
- await mock.setup();
10
- }
11
- }
@@ -1,308 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { z } from 'zod';
3
- import { existsSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
- // Mock dependencies
7
- vi.mock('node:child_process', () => ({
8
- spawn: vi.fn()
9
- }));
10
- vi.mock('node:fs');
11
- vi.mock('node:os');
12
- vi.mock('node:path', () => ({
13
- resolve: vi.fn((path) => path),
14
- join: vi.fn((...args) => args.join('/')),
15
- isAbsolute: vi.fn((path) => path.startsWith('/'))
16
- }));
17
- vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
18
- Server: vi.fn().mockImplementation(function () {
19
- this.setRequestHandler = vi.fn();
20
- this.connect = vi.fn();
21
- this.close = vi.fn();
22
- this.onerror = undefined;
23
- return this;
24
- }),
25
- }));
26
- vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
27
- ListToolsRequestSchema: { name: 'listTools' },
28
- CallToolRequestSchema: { name: 'callTool' },
29
- ErrorCode: {
30
- InternalError: 'InternalError',
31
- MethodNotFound: 'MethodNotFound',
32
- InvalidParams: 'InvalidParams'
33
- },
34
- McpError: class McpError extends Error {
35
- code;
36
- constructor(code, message) {
37
- super(message);
38
- this.code = code;
39
- this.name = 'McpError';
40
- }
41
- }
42
- }));
43
- const mockExistsSync = vi.mocked(existsSync);
44
- const mockHomedir = vi.mocked(homedir);
45
- describe('Argument Validation Tests', () => {
46
- let consoleErrorSpy;
47
- let errorHandler = null;
48
- function setupServerMock() {
49
- errorHandler = null;
50
- vi.mocked(Server).mockImplementation(function () {
51
- this.setRequestHandler = vi.fn();
52
- this.connect = vi.fn();
53
- this.close = vi.fn();
54
- Object.defineProperty(this, 'onerror', {
55
- get() { return errorHandler; },
56
- set(handler) { errorHandler = handler; },
57
- enumerable: true,
58
- configurable: true
59
- });
60
- return this;
61
- });
62
- }
63
- beforeEach(() => {
64
- vi.clearAllMocks();
65
- vi.resetModules();
66
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
67
- // Set up process.env
68
- process.env = { ...process.env };
69
- });
70
- describe('Tool Arguments Schema', () => {
71
- it('should validate valid arguments', async () => {
72
- mockHomedir.mockReturnValue('/home/user');
73
- mockExistsSync.mockReturnValue(true);
74
- setupServerMock();
75
- vi.doUnmock('../server.js');
76
- const module = await import('../server.js');
77
- // @ts-ignore
78
- const { ClaudeCodeServer } = module;
79
- const server = new ClaudeCodeServer();
80
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
81
- // Find tool definition
82
- const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'listTools');
83
- const listHandler = listToolsCall[1];
84
- const tools = await listHandler();
85
- const claudeCodeTool = tools.tools[0];
86
- // Extract schema from tool definition
87
- const schema = z.object({
88
- prompt: z.string(),
89
- workFolder: z.string(),
90
- model: z.string().optional(),
91
- reasoning_effort: z.string().optional(),
92
- session_id: z.string().optional()
93
- });
94
- // Test valid cases
95
- expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp' })).not.toThrow();
96
- expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp', model: 'sonnet' })).not.toThrow();
97
- });
98
- it('should reject invalid arguments', async () => {
99
- mockHomedir.mockReturnValue('/home/user');
100
- mockExistsSync.mockReturnValue(true);
101
- setupServerMock();
102
- vi.doUnmock('../server.js');
103
- const module = await import('../server.js');
104
- // @ts-ignore
105
- const { ClaudeCodeServer } = module;
106
- const server = new ClaudeCodeServer();
107
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
108
- // Find tool definition
109
- const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'listTools');
110
- const listHandler = listToolsCall[1];
111
- const tools = await listHandler();
112
- const claudeCodeTool = tools.tools[0];
113
- // Extract schema from tool definition
114
- const schema = z.object({
115
- prompt: z.string(),
116
- workFolder: z.string(),
117
- model: z.string().optional(),
118
- reasoning_effort: z.string().optional(),
119
- session_id: z.string().optional()
120
- });
121
- // Test invalid cases
122
- expect(() => schema.parse({})).toThrow(); // Missing prompt and workFolder
123
- expect(() => schema.parse({ prompt: 'test' })).toThrow(); // Missing workFolder
124
- expect(() => schema.parse({ prompt: 123, workFolder: '/tmp' })).toThrow(); // Wrong prompt type
125
- expect(() => schema.parse({ prompt: 'test', workFolder: 123 })).toThrow(); // Wrong workFolder type
126
- });
127
- it('should handle missing required fields', async () => {
128
- const schema = z.object({
129
- prompt: z.string(),
130
- workFolder: z.string(),
131
- model: z.string().optional(),
132
- reasoning_effort: z.string().optional(),
133
- session_id: z.string().optional()
134
- });
135
- try {
136
- schema.parse({});
137
- }
138
- catch (error) {
139
- // Both prompt and workFolder are required
140
- expect(error.errors.length).toBe(2);
141
- expect(error.errors.some((e) => e.path[0] === 'prompt')).toBe(true);
142
- expect(error.errors.some((e) => e.path[0] === 'workFolder')).toBe(true);
143
- }
144
- });
145
- it('should allow optional fields to be undefined', async () => {
146
- const schema = z.object({
147
- prompt: z.string(),
148
- workFolder: z.string(),
149
- model: z.string().optional(),
150
- reasoning_effort: z.string().optional(),
151
- session_id: z.string().optional()
152
- });
153
- const result = schema.parse({ prompt: 'test', workFolder: '/tmp' });
154
- expect(result.model).toBeUndefined();
155
- expect(result.session_id).toBeUndefined();
156
- });
157
- it('should handle extra fields gracefully', async () => {
158
- const schema = z.object({
159
- prompt: z.string(),
160
- workFolder: z.string(),
161
- model: z.string().optional(),
162
- reasoning_effort: z.string().optional(),
163
- session_id: z.string().optional()
164
- });
165
- // By default, Zod strips unknown keys
166
- const result = schema.parse({
167
- prompt: 'test',
168
- workFolder: '/tmp',
169
- extraField: 'ignored'
170
- });
171
- expect(result).toEqual({ prompt: 'test', workFolder: '/tmp' });
172
- expect(result).not.toHaveProperty('extraField');
173
- });
174
- });
175
- describe('Runtime Argument Validation', () => {
176
- let handlers;
177
- let mockServerInstance;
178
- async function setupServer() {
179
- // Reset modules to ensure fresh import
180
- vi.resetModules();
181
- // Re-setup mocks after reset
182
- const { existsSync } = await import('node:fs');
183
- const { homedir } = await import('node:os');
184
- vi.mocked(existsSync).mockReturnValue(true);
185
- vi.mocked(homedir).mockReturnValue('/home/user');
186
- const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
187
- vi.mocked(Server).mockImplementation(function () {
188
- mockServerInstance = {
189
- setRequestHandler: vi.fn((schema, handler) => {
190
- handlers.set(schema.name, handler);
191
- }),
192
- connect: vi.fn(),
193
- close: vi.fn(),
194
- onerror: undefined
195
- };
196
- return mockServerInstance;
197
- });
198
- vi.doUnmock('../server.js');
199
- const module = await import('../server.js');
200
- // @ts-ignore
201
- const { ClaudeCodeServer } = module;
202
- const server = new ClaudeCodeServer();
203
- return { server, handlers };
204
- }
205
- beforeEach(() => {
206
- handlers = new Map();
207
- // Re-setup mocks after vi.resetModules() in outer beforeEach
208
- mockHomedir.mockReturnValue('/home/user');
209
- mockExistsSync.mockReturnValue(true);
210
- });
211
- it('should validate workFolder is a string when provided', async () => {
212
- mockHomedir.mockReturnValue('/home/user');
213
- mockExistsSync.mockReturnValue(true);
214
- await setupServer();
215
- const handler = handlers.get('callTool');
216
- // Test with non-string workFolder
217
- await expect(handler({
218
- params: {
219
- name: 'run',
220
- arguments: {
221
- prompt: 'test',
222
- workFolder: 123 // Invalid type
223
- }
224
- }
225
- })).rejects.toThrow();
226
- });
227
- it('should reject empty string prompt', async () => {
228
- await setupServer();
229
- const handler = handlers.get('callTool');
230
- // Empty string prompt should be rejected
231
- await expect(handler({
232
- params: {
233
- name: 'run',
234
- arguments: {
235
- prompt: '', // Empty prompt
236
- workFolder: '/tmp'
237
- }
238
- }
239
- })).rejects.toThrow('Either prompt or prompt_file must be provided');
240
- });
241
- it('should reject invalid reasoning_effort values', async () => {
242
- await setupServer();
243
- const handler = handlers.get('callTool');
244
- await expect(handler({
245
- params: {
246
- name: 'run',
247
- arguments: {
248
- prompt: 'test',
249
- workFolder: '/tmp',
250
- model: 'gpt-5.2-codex',
251
- reasoning_effort: 'fast'
252
- }
253
- }
254
- })).rejects.toThrow(/reasoning_effort/i);
255
- });
256
- it('should reject reasoning_effort for unsupported model families', async () => {
257
- await setupServer();
258
- const handler = handlers.get('callTool');
259
- await expect(handler({
260
- params: {
261
- name: 'run',
262
- arguments: {
263
- prompt: 'test',
264
- workFolder: '/tmp',
265
- model: 'gemini-2.5-pro',
266
- reasoning_effort: 'low'
267
- }
268
- }
269
- })).rejects.toThrow(/reasoning_effort/i);
270
- });
271
- it.each([
272
- 'oc-',
273
- 'oc-openai',
274
- 'oc-/gpt-5.4',
275
- 'oc-openai/',
276
- ' oc-openai/gpt-5.4',
277
- 'oc-openai/gpt-5.4 ',
278
- ])('should reject malformed OpenCode model syntax at runtime: %s', async (model) => {
279
- await setupServer();
280
- const handler = handlers.get('callTool');
281
- await expect(handler({
282
- params: {
283
- name: 'run',
284
- arguments: {
285
- prompt: 'test',
286
- workFolder: '/tmp',
287
- model,
288
- }
289
- }
290
- })).rejects.toThrow('Invalid OpenCode model. Expected exact syntax oc-<provider/model>.');
291
- });
292
- it('should reject reasoning_effort for OpenCode runtime requests', async () => {
293
- await setupServer();
294
- const handler = handlers.get('callTool');
295
- await expect(handler({
296
- params: {
297
- name: 'run',
298
- arguments: {
299
- prompt: 'test',
300
- workFolder: '/tmp',
301
- model: 'opencode',
302
- reasoning_effort: 'high',
303
- }
304
- }
305
- })).rejects.toThrow('reasoning_effort is not supported for opencode.');
306
- });
307
- });
308
- });