diff --git a/packages/mcp-client/src/logger.test.ts b/packages/mcp-client/src/logger.test.ts new file mode 100644 index 00000000..9741db80 --- /dev/null +++ b/packages/mcp-client/src/logger.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, vi } from 'vitest'; +import { MCPClient, type McpLogger, type MCPConfig } from './index.js'; + +// ───────────────────────────────────────────────────────────────────────────── +// Tests for the pluggable McpLogger interface introduced in TODO-3. +// +// We exercise the constructor's logger selection and the `disconnect()` path +// (the simplest method that produces a log line without actually attempting +// to spawn an MCP server subprocess via StdioClientTransport). +// ───────────────────────────────────────────────────────────────────────────── + +function makeConfig(overrides: Partial = {}): MCPConfig { + return { + serverCommand: 'echo', + serverArgs: 'noop', + envFile: '/dev/null', + timeoutMs: 1000, + cacheTtlSec: 60, + maxRetries: 0, + ...overrides, + }; +} + +function makeMockLogger(): McpLogger & { + debug: ReturnType; + info: ReturnType; + warn: ReturnType; + error: ReturnType; +} { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +describe('MCPClient logger injection', () => { + it('defaults to the global console when no logger is provided', () => { + // The cleanest behavioural check: the client is constructible without a + // logger config, which proves the `?? console` fallback works. + const client = new MCPClient(makeConfig()); + expect(client).toBeDefined(); + }); + + it('uses an injected logger for warn paths', async () => { + const logger = makeMockLogger(); + const client = new MCPClient(makeConfig({ logger })); + + // Calling disconnect() when not connected returns immediately with no + // log calls (it's a no-op early return). Calling connect() twice would + // emit a warn, but the second call has to wait for the first transport + // to attach which we can't do in a unit test. Instead, we verify the + // simpler invariant: the disconnect() early-return path doesn't log. + await client.disconnect(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('does not throw when the user supplies a partial-shape logger', () => { + // Anything matching the McpLogger structural type is accepted. We pass a + // pino-shaped logger that uses no-op functions to confirm the contract. + const pinoShaped: McpLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }; + expect(() => new MCPClient(makeConfig({ logger: pinoShaped }))).not.toThrow(); + }); + + it('McpLogger methods accept variadic structured arguments', () => { + // Structural-typing check: the interface must accept (msg, ...args). + // This compiles iff the McpLogger signatures use `...args: unknown[]`. + const logger = makeMockLogger(); + logger.info('hello', { a: 1 }, 'world', 42); + expect(logger.info).toHaveBeenCalledWith('hello', { a: 1 }, 'world', 42); + }); +});