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,369 +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
|
-
|
|
7
|
-
// Mock dependencies
|
|
8
|
-
vi.mock('node:child_process', () => ({
|
|
9
|
-
spawn: vi.fn()
|
|
10
|
-
}));
|
|
11
|
-
vi.mock('node:fs');
|
|
12
|
-
vi.mock('node:os');
|
|
13
|
-
vi.mock('node:path', () => ({
|
|
14
|
-
resolve: vi.fn((path) => path),
|
|
15
|
-
join: vi.fn((...args) => args.join('/')),
|
|
16
|
-
isAbsolute: vi.fn((path) => path.startsWith('/'))
|
|
17
|
-
}));
|
|
18
|
-
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
19
|
-
Server: vi.fn().mockImplementation(function(this: any) {
|
|
20
|
-
this.setRequestHandler = vi.fn();
|
|
21
|
-
this.connect = vi.fn();
|
|
22
|
-
this.close = vi.fn();
|
|
23
|
-
this.onerror = undefined;
|
|
24
|
-
return this;
|
|
25
|
-
}),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
29
|
-
ListToolsRequestSchema: { name: 'listTools' },
|
|
30
|
-
CallToolRequestSchema: { name: 'callTool' },
|
|
31
|
-
ErrorCode: {
|
|
32
|
-
InternalError: 'InternalError',
|
|
33
|
-
MethodNotFound: 'MethodNotFound',
|
|
34
|
-
InvalidParams: 'InvalidParams'
|
|
35
|
-
},
|
|
36
|
-
McpError: class McpError extends Error {
|
|
37
|
-
code: string;
|
|
38
|
-
constructor(code: string, message: string) {
|
|
39
|
-
super(message);
|
|
40
|
-
this.code = code;
|
|
41
|
-
this.name = 'McpError';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
const mockExistsSync = vi.mocked(existsSync);
|
|
47
|
-
const mockHomedir = vi.mocked(homedir);
|
|
48
|
-
|
|
49
|
-
describe('Argument Validation Tests', () => {
|
|
50
|
-
let consoleErrorSpy: any;
|
|
51
|
-
let errorHandler: any = null;
|
|
52
|
-
|
|
53
|
-
function setupServerMock() {
|
|
54
|
-
errorHandler = null;
|
|
55
|
-
vi.mocked(Server).mockImplementation(function(this: any) {
|
|
56
|
-
this.setRequestHandler = vi.fn();
|
|
57
|
-
this.connect = vi.fn();
|
|
58
|
-
this.close = vi.fn();
|
|
59
|
-
Object.defineProperty(this, 'onerror', {
|
|
60
|
-
get() { return errorHandler; },
|
|
61
|
-
set(handler) { errorHandler = handler; },
|
|
62
|
-
enumerable: true,
|
|
63
|
-
configurable: true
|
|
64
|
-
});
|
|
65
|
-
return this;
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
beforeEach(() => {
|
|
70
|
-
vi.clearAllMocks();
|
|
71
|
-
vi.resetModules();
|
|
72
|
-
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
73
|
-
// Set up process.env
|
|
74
|
-
process.env = { ...process.env };
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('Tool Arguments Schema', () => {
|
|
78
|
-
it('should validate valid arguments', async () => {
|
|
79
|
-
mockHomedir.mockReturnValue('/home/user');
|
|
80
|
-
mockExistsSync.mockReturnValue(true);
|
|
81
|
-
setupServerMock();
|
|
82
|
-
vi.doUnmock('../server.js');
|
|
83
|
-
const module = await import('../server.js');
|
|
84
|
-
// @ts-ignore
|
|
85
|
-
const { ClaudeCodeServer } = module;
|
|
86
|
-
|
|
87
|
-
const server = new ClaudeCodeServer();
|
|
88
|
-
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
89
|
-
|
|
90
|
-
// Find tool definition
|
|
91
|
-
const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find(
|
|
92
|
-
(call: any[]) => call[0].name === 'listTools'
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const listHandler = listToolsCall[1];
|
|
96
|
-
const tools = await listHandler();
|
|
97
|
-
const claudeCodeTool = tools.tools[0];
|
|
98
|
-
|
|
99
|
-
// Extract schema from tool definition
|
|
100
|
-
const schema = z.object({
|
|
101
|
-
prompt: z.string(),
|
|
102
|
-
workFolder: z.string(),
|
|
103
|
-
model: z.string().optional(),
|
|
104
|
-
reasoning_effort: z.string().optional(),
|
|
105
|
-
session_id: z.string().optional()
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Test valid cases
|
|
109
|
-
expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp' })).not.toThrow();
|
|
110
|
-
expect(() => schema.parse({ prompt: 'test', workFolder: '/tmp', model: 'sonnet' })).not.toThrow();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should reject invalid arguments', async () => {
|
|
114
|
-
mockHomedir.mockReturnValue('/home/user');
|
|
115
|
-
mockExistsSync.mockReturnValue(true);
|
|
116
|
-
setupServerMock();
|
|
117
|
-
vi.doUnmock('../server.js');
|
|
118
|
-
const module = await import('../server.js');
|
|
119
|
-
// @ts-ignore
|
|
120
|
-
const { ClaudeCodeServer } = module;
|
|
121
|
-
|
|
122
|
-
const server = new ClaudeCodeServer();
|
|
123
|
-
const mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
124
|
-
|
|
125
|
-
// Find tool definition
|
|
126
|
-
const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find(
|
|
127
|
-
(call: any[]) => call[0].name === 'listTools'
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const listHandler = listToolsCall[1];
|
|
131
|
-
const tools = await listHandler();
|
|
132
|
-
const claudeCodeTool = tools.tools[0];
|
|
133
|
-
|
|
134
|
-
// Extract schema from tool definition
|
|
135
|
-
const schema = z.object({
|
|
136
|
-
prompt: z.string(),
|
|
137
|
-
workFolder: z.string(),
|
|
138
|
-
model: z.string().optional(),
|
|
139
|
-
reasoning_effort: z.string().optional(),
|
|
140
|
-
session_id: z.string().optional()
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Test invalid cases
|
|
144
|
-
expect(() => schema.parse({})).toThrow(); // Missing prompt and workFolder
|
|
145
|
-
expect(() => schema.parse({ prompt: 'test' })).toThrow(); // Missing workFolder
|
|
146
|
-
expect(() => schema.parse({ prompt: 123, workFolder: '/tmp' })).toThrow(); // Wrong prompt type
|
|
147
|
-
expect(() => schema.parse({ prompt: 'test', workFolder: 123 })).toThrow(); // Wrong workFolder type
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should handle missing required fields', async () => {
|
|
151
|
-
const schema = z.object({
|
|
152
|
-
prompt: z.string(),
|
|
153
|
-
workFolder: z.string(),
|
|
154
|
-
model: z.string().optional(),
|
|
155
|
-
reasoning_effort: z.string().optional(),
|
|
156
|
-
session_id: z.string().optional()
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
schema.parse({});
|
|
161
|
-
} catch (error: any) {
|
|
162
|
-
// Both prompt and workFolder are required
|
|
163
|
-
expect(error.errors.length).toBe(2);
|
|
164
|
-
expect(error.errors.some((e: any) => e.path[0] === 'prompt')).toBe(true);
|
|
165
|
-
expect(error.errors.some((e: any) => e.path[0] === 'workFolder')).toBe(true);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should allow optional fields to be undefined', async () => {
|
|
170
|
-
const schema = z.object({
|
|
171
|
-
prompt: z.string(),
|
|
172
|
-
workFolder: z.string(),
|
|
173
|
-
model: z.string().optional(),
|
|
174
|
-
reasoning_effort: z.string().optional(),
|
|
175
|
-
session_id: z.string().optional()
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const result = schema.parse({ prompt: 'test', workFolder: '/tmp' });
|
|
179
|
-
expect(result.model).toBeUndefined();
|
|
180
|
-
expect(result.session_id).toBeUndefined();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should handle extra fields gracefully', async () => {
|
|
184
|
-
const schema = z.object({
|
|
185
|
-
prompt: z.string(),
|
|
186
|
-
workFolder: z.string(),
|
|
187
|
-
model: z.string().optional(),
|
|
188
|
-
reasoning_effort: z.string().optional(),
|
|
189
|
-
session_id: z.string().optional()
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// By default, Zod strips unknown keys
|
|
193
|
-
const result = schema.parse({
|
|
194
|
-
prompt: 'test',
|
|
195
|
-
workFolder: '/tmp',
|
|
196
|
-
extraField: 'ignored'
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
expect(result).toEqual({ prompt: 'test', workFolder: '/tmp' });
|
|
200
|
-
expect(result).not.toHaveProperty('extraField');
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
describe('Runtime Argument Validation', () => {
|
|
205
|
-
let handlers: Map<string, Function>;
|
|
206
|
-
let mockServerInstance: any;
|
|
207
|
-
|
|
208
|
-
async function setupServer() {
|
|
209
|
-
// Reset modules to ensure fresh import
|
|
210
|
-
vi.resetModules();
|
|
211
|
-
|
|
212
|
-
// Re-setup mocks after reset
|
|
213
|
-
const { existsSync } = await import('node:fs');
|
|
214
|
-
const { homedir } = await import('node:os');
|
|
215
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
216
|
-
vi.mocked(homedir).mockReturnValue('/home/user');
|
|
217
|
-
|
|
218
|
-
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
219
|
-
|
|
220
|
-
vi.mocked(Server).mockImplementation(function(this: any) {
|
|
221
|
-
mockServerInstance = {
|
|
222
|
-
setRequestHandler: vi.fn((schema: any, handler: Function) => {
|
|
223
|
-
handlers.set(schema.name, handler);
|
|
224
|
-
}),
|
|
225
|
-
connect: vi.fn(),
|
|
226
|
-
close: vi.fn(),
|
|
227
|
-
onerror: undefined
|
|
228
|
-
};
|
|
229
|
-
return mockServerInstance as any;
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
vi.doUnmock('../server.js');
|
|
233
|
-
const module = await import('../server.js');
|
|
234
|
-
// @ts-ignore
|
|
235
|
-
const { ClaudeCodeServer } = module;
|
|
236
|
-
|
|
237
|
-
const server = new ClaudeCodeServer();
|
|
238
|
-
return { server, handlers };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
beforeEach(() => {
|
|
242
|
-
handlers = new Map();
|
|
243
|
-
// Re-setup mocks after vi.resetModules() in outer beforeEach
|
|
244
|
-
mockHomedir.mockReturnValue('/home/user');
|
|
245
|
-
mockExistsSync.mockReturnValue(true);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should validate workFolder is a string when provided', async () => {
|
|
249
|
-
mockHomedir.mockReturnValue('/home/user');
|
|
250
|
-
mockExistsSync.mockReturnValue(true);
|
|
251
|
-
|
|
252
|
-
await setupServer();
|
|
253
|
-
const handler = handlers.get('callTool')!;
|
|
254
|
-
|
|
255
|
-
// Test with non-string workFolder
|
|
256
|
-
await expect(
|
|
257
|
-
handler({
|
|
258
|
-
params: {
|
|
259
|
-
name: 'run',
|
|
260
|
-
arguments: {
|
|
261
|
-
prompt: 'test',
|
|
262
|
-
workFolder: 123 // Invalid type
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
).rejects.toThrow();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('should reject empty string prompt', async () => {
|
|
270
|
-
await setupServer();
|
|
271
|
-
const handler = handlers.get('callTool')!;
|
|
272
|
-
|
|
273
|
-
// Empty string prompt should be rejected
|
|
274
|
-
await expect(
|
|
275
|
-
handler({
|
|
276
|
-
params: {
|
|
277
|
-
name: 'run',
|
|
278
|
-
arguments: {
|
|
279
|
-
prompt: '', // Empty prompt
|
|
280
|
-
workFolder: '/tmp'
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
).rejects.toThrow('Either prompt or prompt_file must be provided');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('should reject invalid reasoning_effort values', async () => {
|
|
288
|
-
await setupServer();
|
|
289
|
-
const handler = handlers.get('callTool')!;
|
|
290
|
-
|
|
291
|
-
await expect(
|
|
292
|
-
handler({
|
|
293
|
-
params: {
|
|
294
|
-
name: 'run',
|
|
295
|
-
arguments: {
|
|
296
|
-
prompt: 'test',
|
|
297
|
-
workFolder: '/tmp',
|
|
298
|
-
model: 'gpt-5.2-codex',
|
|
299
|
-
reasoning_effort: 'fast'
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
})
|
|
303
|
-
).rejects.toThrow(/reasoning_effort/i);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('should reject reasoning_effort for unsupported model families', async () => {
|
|
307
|
-
await setupServer();
|
|
308
|
-
const handler = handlers.get('callTool')!;
|
|
309
|
-
|
|
310
|
-
await expect(
|
|
311
|
-
handler({
|
|
312
|
-
params: {
|
|
313
|
-
name: 'run',
|
|
314
|
-
arguments: {
|
|
315
|
-
prompt: 'test',
|
|
316
|
-
workFolder: '/tmp',
|
|
317
|
-
model: 'gemini-2.5-pro',
|
|
318
|
-
reasoning_effort: 'low'
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
})
|
|
322
|
-
).rejects.toThrow(/reasoning_effort/i);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it.each([
|
|
326
|
-
'oc-',
|
|
327
|
-
'oc-openai',
|
|
328
|
-
'oc-/gpt-5.4',
|
|
329
|
-
'oc-openai/',
|
|
330
|
-
' oc-openai/gpt-5.4',
|
|
331
|
-
'oc-openai/gpt-5.4 ',
|
|
332
|
-
])('should reject malformed OpenCode model syntax at runtime: %s', async (model) => {
|
|
333
|
-
await setupServer();
|
|
334
|
-
const handler = handlers.get('callTool')!;
|
|
335
|
-
|
|
336
|
-
await expect(
|
|
337
|
-
handler({
|
|
338
|
-
params: {
|
|
339
|
-
name: 'run',
|
|
340
|
-
arguments: {
|
|
341
|
-
prompt: 'test',
|
|
342
|
-
workFolder: '/tmp',
|
|
343
|
-
model,
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
).rejects.toThrow('Invalid OpenCode model. Expected exact syntax oc-<provider/model>.');
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('should reject reasoning_effort for OpenCode runtime requests', async () => {
|
|
351
|
-
await setupServer();
|
|
352
|
-
const handler = handlers.get('callTool')!;
|
|
353
|
-
|
|
354
|
-
await expect(
|
|
355
|
-
handler({
|
|
356
|
-
params: {
|
|
357
|
-
name: 'run',
|
|
358
|
-
arguments: {
|
|
359
|
-
prompt: 'test',
|
|
360
|
-
workFolder: '/tmp',
|
|
361
|
-
model: 'opencode',
|
|
362
|
-
reasoning_effort: 'high',
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
})
|
|
366
|
-
).rejects.toThrow('reasoning_effort is not supported for opencode.');
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } 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 } from './utils/persistent-mock.js';
|
|
7
|
-
|
|
8
|
-
describe('Version Print on First Use', () => {
|
|
9
|
-
let client: MCPTestClient;
|
|
10
|
-
let testDir: string;
|
|
11
|
-
let consoleErrorSpy: any;
|
|
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
|
-
// Spy on console.error
|
|
21
|
-
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
22
|
-
|
|
23
|
-
client = createTestClient({ debug: false });
|
|
24
|
-
await client.connect();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
// Disconnect client
|
|
29
|
-
await client.disconnect();
|
|
30
|
-
|
|
31
|
-
// Clean up test directory
|
|
32
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
33
|
-
|
|
34
|
-
// Restore console.error spy
|
|
35
|
-
consoleErrorSpy.mockRestore();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should print version and startup time only on first use', async () => {
|
|
39
|
-
// First tool call
|
|
40
|
-
await client.callTool('run', {
|
|
41
|
-
prompt: 'echo "test 1"',
|
|
42
|
-
workFolder: testDir,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Find the version print in the console.error calls
|
|
46
|
-
const findVersionCall = (calls: any[][]) => {
|
|
47
|
-
return calls.find(call => {
|
|
48
|
-
const str = call[1] || call[0]; // message might be first or second param
|
|
49
|
-
return typeof str === 'string' && str.includes('ai_cli_mcp v') && str.includes('started at');
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Check that version was printed on first use
|
|
54
|
-
const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
55
|
-
expect(versionCall).toBeDefined();
|
|
56
|
-
expect(versionCall![1]).toMatch(/ai_cli_mcp v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
|
|
57
|
-
|
|
58
|
-
// Clear the spy but keep the spy active
|
|
59
|
-
consoleErrorSpy.mockClear();
|
|
60
|
-
|
|
61
|
-
// Second tool call
|
|
62
|
-
await client.callTool('run', {
|
|
63
|
-
prompt: 'echo "test 2"',
|
|
64
|
-
workFolder: testDir,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Check that version was NOT printed on second use
|
|
68
|
-
const secondVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
69
|
-
expect(secondVersionCall).toBeUndefined();
|
|
70
|
-
|
|
71
|
-
// Third tool call
|
|
72
|
-
await client.callTool('run', {
|
|
73
|
-
prompt: 'echo "test 3"',
|
|
74
|
-
workFolder: testDir,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Should still not have been called with version print
|
|
78
|
-
const thirdVersionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
79
|
-
expect(thirdVersionCall).toBeUndefined();
|
|
80
|
-
});
|
|
81
|
-
});
|