import { describe, expect, it } from "vitest"; import { McpManager, type McpManagerDeps } from "./manager.js"; import type { Connection } from "./transport.js"; import type { ResolvedMcpServer } from "./types.js"; function makeMockConnection(): Connection { return { send: async (method: string) => { if (method === "initialize") { return { protocolVersion: "2025-11-25", capabilities: { tools: { listChanged: false } }, serverInfo: { name: "test", version: "1.0.0" }, }; } if (method === "tools/list") { return { tools: [ { name: "tool_a", description: "Tool A", inputSchema: { type: "object" }, }, ], }; } return {}; }, notify: () => {}, onNotification: () => {}, close: () => {}, pid: 100, }; } function makeBrokenConnection(): Connection { return { send: async () => { throw new Error("Connection refused"); }, notify: () => {}, onNotification: () => {}, close: () => {}, pid: 101, }; } function makeManager( connectionFactory: (_server: ResolvedMcpServer) => { connection: Connection; promise: Promise; }, ): McpManager { const currentTime = 1000; const deps: McpManagerDeps = { spawn: () => ({ stdin: { write: () => {} }, stdout: { on: () => {} }, pid: 1, kill: () => {}, }), now: () => currentTime, }; const factory = (_server: ResolvedMcpServer, _cwd: string) => connectionFactory(_server); const manager = new McpManager(deps, factory); return manager; } const testServer: ResolvedMcpServer = { id: "test-server", command: ["test-cmd"], configSource: ".dispatch/mcp.json", }; describe("McpManager", () => { it("lazy-spawn on first access", async () => { const conn = makeMockConnection(); let spawnCount = 0; const manager = makeManager((_server) => { spawnCount++; return { connection: conn, promise: Promise.resolve() }; }); const client = await manager.ensureConnected(testServer, "/tmp"); expect(client).toBeDefined(); expect(spawnCount).toBe(1); }); it("reuses existing client on second access", async () => { const conn = makeMockConnection(); let spawnCount = 0; const manager = makeManager(() => { spawnCount++; return { connection: conn, promise: Promise.resolve() }; }); const client1 = await manager.ensureConnected(testServer, "/tmp"); const client2 = await manager.ensureConnected(testServer, "/tmp"); expect(client1).toBe(client2); expect(spawnCount).toBe(1); }); it("status returns server states", async () => { const conn = makeMockConnection(); const manager = makeManager(() => { return { connection: conn, promise: Promise.resolve() }; }); // Before connecting let statuses = manager.status([testServer]); expect(statuses.length).toBe(1); expect(statuses[0].state).toBe("disconnected"); // After connecting await manager.ensureConnected(testServer, "/tmp"); statuses = manager.status([testServer]); expect(statuses.length).toBe(1); expect(statuses[0].state).toBe("connected"); expect(statuses[0].toolCount).toBe(1); }); it("shutdownAll kills all clients", async () => { let closed = false; const conn: Connection = { send: async (method: string) => { if (method === "initialize") { return { protocolVersion: "2025-11-25", capabilities: {}, serverInfo: { name: "test", version: "1.0.0" }, }; } if (method === "tools/list") return { tools: [] }; return {}; }, notify: () => {}, onNotification: () => {}, close: () => { closed = true; }, pid: 200, }; const manager = makeManager(() => { return { connection: conn, promise: Promise.resolve() }; }); await manager.ensureConnected(testServer, "/tmp"); manager.shutdownAll(); expect(closed).toBe(true); const statuses = manager.status([testServer]); expect(statuses[0].state).toBe("disconnected"); }); it("broken server reports error state", async () => { const manager = makeManager(() => { return { connection: makeBrokenConnection(), promise: Promise.resolve() }; }); await expect(manager.ensureConnected(testServer, "/tmp")).rejects.toThrow(); const statuses = manager.status([testServer]); expect(statuses[0].state).toBe("error"); expect(statuses[0].error).toContain("test-server"); }); it("broken server retries after backoff", async () => { let currentTime = 1000; const brokenConn = makeBrokenConnection(); const goodConn = makeMockConnection(); let useGood = false; const deps: McpManagerDeps = { spawn: () => ({ stdin: { write: () => {} }, stdout: { on: () => {} }, pid: 1, kill: () => {}, }), now: () => currentTime, }; const factory = (_server: ResolvedMcpServer, _cwd: string) => { const conn = useGood ? goodConn : brokenConn; return { connection: conn, promise: Promise.resolve() }; }; const manager = new McpManager(deps, factory); // First attempt fails await expect(manager.ensureConnected(testServer, "/tmp")).rejects.toThrow(); expect(manager.status([testServer])[0].state).toBe("error"); // Not enough time passed — still broken currentTime += 29_000; expect(manager.status([testServer])[0].state).toBe("error"); // After backoff — should allow retry useGood = true; currentTime += 2_000; const statuses = manager.status([testServer]); // After backoff, status() clears the broken entry expect(statuses[0].state).toBe("disconnected"); }); it("getClient returns undefined for unknown server", () => { const manager = makeManager(() => { return { connection: makeMockConnection(), promise: Promise.resolve() }; }); expect(manager.getClient("nonexistent")).toBeUndefined(); }); });