import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createTestCommand } from '../../src/commands/test-mcp.js'; function makeSession(overrides: Partial<{ initialize: () => Promise; listTools: () => Promise>; callTool: (name: string, args: Record) => Promise; close: () => Promise; }> = {}) { return { initialize: overrides.initialize ?? vi.fn(async () => ({ protocolVersion: '2024-11-05' })), listTools: overrides.listTools ?? vi.fn(async () => [{ name: 'echo' }, { name: 'search' }]), callTool: overrides.callTool ?? vi.fn(async () => ({ content: [{ type: 'text', text: 'hi' }] })), close: overrides.close ?? vi.fn(async () => { /* no-op */ }), }; } describe('mcpctl test mcp', () => { const output: string[] = []; const log = (...args: unknown[]) => { output.push(args.map(String).join(' ')); }; beforeEach(() => { output.length = 0; process.exitCode = 0; }); afterEach(() => { process.exitCode = 0; }); it('exits 0 on happy path (health + initialize + tools/list)', async () => { const session = makeSession(); const cmd = createTestCommand({ log, createSession: () => session, healthCheck: async () => true, }); await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' }); expect(process.exitCode).toBe(0); expect(session.initialize).toHaveBeenCalled(); expect(session.listTools).toHaveBeenCalled(); expect(output.join('\n')).toContain('Result: PASS'); }); it('exits 1 when the /healthz preflight fails', async () => { const cmd = createTestCommand({ log, createSession: () => makeSession(), healthCheck: async () => false, }); await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' }); expect(process.exitCode).toBe(1); expect(output.join('\n')).toContain('healthz preflight failed'); }); it('exits 2 (contract fail) when --expect-tools are missing', async () => { const cmd = createTestCommand({ log, createSession: () => makeSession({ listTools: async () => [{ name: 'echo' }], }), healthCheck: async () => true, }); await cmd.parseAsync( ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--expect-tools', 'echo,search'], { from: 'user' }, ); expect(process.exitCode).toBe(2); expect(output.join('\n')).toContain('Missing: search'); expect(output.join('\n')).toContain('CONTRACT FAIL'); }); it('exits 0 when --expect-tools all match', async () => { const cmd = createTestCommand({ log, createSession: () => makeSession({ listTools: async () => [{ name: 'echo' }, { name: 'search' }, { name: 'x' }], }), healthCheck: async () => true, }); await cmd.parseAsync( ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--expect-tools', 'echo,search'], { from: 'user' }, ); expect(process.exitCode).toBe(0); }); it('exits 1 on transport/auth failure (initialize throws)', async () => { const cmd = createTestCommand({ log, createSession: () => makeSession({ initialize: async () => { throw new Error('HTTP 401: unauthorized'); }, }), healthCheck: async () => true, }); await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' }); expect(process.exitCode).toBe(1); expect(output.join('\n')).toContain('Error:'); expect(output.join('\n')).toContain('TRANSPORT/AUTH FAIL'); }); it('invokes --tool with --args and reports isError', async () => { const callTool = vi.fn(async () => ({ content: [{ type: 'text', text: 'oops' }], isError: true })); const cmd = createTestCommand({ log, createSession: () => makeSession({ callTool }), healthCheck: async () => true, }); await cmd.parseAsync( ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--tool', 'echo', '--args', '{"msg":"hi"}'], { from: 'user' }, ); expect(callTool).toHaveBeenCalledWith('echo', { msg: 'hi' }); expect(process.exitCode).toBe(2); }); it('outputs a JSON report with -o json', async () => { const cmd = createTestCommand({ log, createSession: () => makeSession(), healthCheck: async () => true, }); await cmd.parseAsync( ['mcp', 'https://mcp.example.com/projects/foo/mcp', '-o', 'json'], { from: 'user' }, ); const parsed = JSON.parse(output.join('\n')) as { exitCode: number; tools: string[] }; expect(parsed.exitCode).toBe(0); expect(parsed.tools).toEqual(['echo', 'search']); }); it('reads $MCPCTL_TOKEN when --token is not given', async () => { let observedBearer: string | undefined; const cmd = createTestCommand({ log, createSession: (_url, opts) => { observedBearer = opts.bearer; return makeSession(); }, healthCheck: async () => true, }); const prev = process.env.MCPCTL_TOKEN; process.env.MCPCTL_TOKEN = 'mcpctl_pat_fromenv'; try { await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' }); } finally { if (prev === undefined) delete process.env.MCPCTL_TOKEN; else process.env.MCPCTL_TOKEN = prev; } expect(observedBearer).toBe('mcpctl_pat_fromenv'); }); it('rejects invalid --args as JSON', async () => { const cmd = createTestCommand({ log, createSession: () => makeSession(), healthCheck: async () => true, }); await cmd.parseAsync( ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--tool', 'echo', '--args', 'not-json'], { from: 'user' }, ); expect(process.exitCode).toBe(1); expect(output.join('\n')).toContain('must be valid JSON'); }); });