import { randomUUID } from 'node:crypto'; import fs from 'node:fs/promises'; import net from 'node:net'; import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ServerDefinition } from '../src/config.js'; import { __testProcessRequest, cleanupDaemonArtifactsIfOwned, isDaemonResponding, metadataMatches, } from '../src/daemon/host.js'; import type { DaemonRequest } from '../src/daemon/protocol.js'; import type { Runtime } from '../src/runtime.js'; describe('daemon host request handling', () => { const metadata = { configPath: '/tmp/config.json', configLayers: [], configMtimeMs: Date.now(), socketPath: '/tmp/socket', startedAt: Date.now(), logPath: null, }; const logContext = { enabled: false, logAllServers: false, servers: new Set() }; it('reuses pre-parsed requests without reparsing payloads', async () => { const parsedRequest: DaemonRequest = { id: '1', method: 'status', params: {} }; const result = await __testProcessRequest( '!!!invalid-json!!!', {} as Runtime, new Map(), new Map(), metadata, logContext, parsedRequest ); expect(result.response.ok).toBe(true); expect(result.shouldShutdown).toBe(false); }); it('defaults daemon callTool and listTools requests to cached auth', async () => { const runtime = createRuntimeDouble(); const managedServers = createManagedServers(); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'call', method: 'callTool', params: { server: 'oauth', tool: 'ping' }, }); expect(runtime.callTool).toHaveBeenCalledWith('oauth', 'ping', { args: {}, timeoutMs: undefined, disableOAuth: false, }); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'list', method: 'listTools', params: { server: 'oauth', includeSchema: true }, }); expect(runtime.listTools).toHaveBeenCalledWith('oauth', { includeSchema: true, autoAuthorize: undefined, allowCachedAuth: true, disableOAuth: false, }); }); it('keeps stdio keep-alive listTools requests reusable when callers disable auto auth', async () => { const runtime = createRuntimeDouble(); const managedServers = createManagedServers(); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'list', method: 'listTools', params: { server: 'local', includeSchema: true, autoAuthorize: false, allowCachedAuth: true }, }); expect(runtime.listTools).toHaveBeenCalledWith('local', { includeSchema: true, autoAuthorize: undefined, allowCachedAuth: true, disableOAuth: false, }); }); it('preserves HTTP listTools auto-auth opt out on daemon requests', async () => { const runtime = createRuntimeDouble(); const managedServers = createManagedServers(); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'list', method: 'listTools', params: { server: 'oauth', includeSchema: true, autoAuthorize: false, allowCachedAuth: true }, }); expect(runtime.listTools).toHaveBeenCalledWith('oauth', { includeSchema: true, autoAuthorize: false, allowCachedAuth: true, disableOAuth: false, }); }); it('forwards disableOAuth on daemon callTool and listTools requests', async () => { const runtime = createRuntimeDouble(); const managedServers = createManagedServers(); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'call', method: 'callTool', params: { server: 'oauth', tool: 'ping', disableOAuth: true }, }); expect(runtime.callTool).toHaveBeenCalledWith('oauth', 'ping', { args: {}, timeoutMs: undefined, disableOAuth: true, }); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'list', method: 'listTools', params: { server: 'oauth', includeSchema: true, disableOAuth: true }, }); expect(runtime.listTools).toHaveBeenCalledWith('oauth', { includeSchema: true, autoAuthorize: undefined, allowCachedAuth: true, disableOAuth: true, }); }); it('preserves explicit listTools cached-auth opt out on daemon requests', async () => { const runtime = createRuntimeDouble(); const managedServers = createManagedServers(); await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, { id: 'list', method: 'listTools', params: { server: 'oauth', allowCachedAuth: false }, }); expect(runtime.listTools).toHaveBeenCalledWith('oauth', { includeSchema: undefined, autoAuthorize: undefined, allowCachedAuth: false, disableOAuth: false, }); }); }); const describeUnixSocket = process.platform === 'win32' ? describe.skip : describe; describeUnixSocket('isDaemonResponding', () => { const servers: net.Server[] = []; const connections: net.Socket[] = []; const socketPaths: string[] = []; function socketPath(): string { const p = path.join(os.tmpdir(), `mcporter-probe-${randomUUID().slice(0, 8)}.sock`); socketPaths.push(p); return p; } function listen(server: net.Server, p: string): Promise { servers.push(server); server.on('connection', (socket) => connections.push(socket)); return new Promise((resolve) => server.listen(p, () => resolve())); } afterEach(async () => { for (const socket of connections.splice(0)) { socket.destroy(); } for (const server of servers.splice(0)) { await new Promise((resolve) => server.close(() => resolve())); } for (const p of socketPaths.splice(0)) { await fs.rm(p, { force: true }).catch(() => {}); } }); it('returns true when the socket answers status with a matching socket and live pid', async () => { const p = socketPath(); await listen(statusServer({ pid: process.pid, socketPath: p }), p); expect(await isDaemonResponding(p)).toBe(true); }); it('returns false when the socket accepts but never responds (hung daemon)', async () => { const p = socketPath(); await listen( net.createServer((socket) => socket.pause()), p ); expect(await isDaemonResponding(p)).toBe(false); }, 5_000); it('returns false when status reports a different socket (foreign listener)', async () => { const p = socketPath(); await listen(statusServer({ pid: process.pid, socketPath: '/some/other/daemon.sock' }), p); expect(await isDaemonResponding(p)).toBe(false); }); it('returns false when status reports a dead pid', async () => { const p = socketPath(); await listen(statusServer({ pid: 2_147_483_646, socketPath: p }), p); expect(await isDaemonResponding(p)).toBe(false); }); it('returns false when nothing is listening', async () => { expect(await isDaemonResponding(socketPath())).toBe(false); }); }); describe('metadataMatches', () => { let metadataPath: string; const live = { pid: 4321, socketPath: '/tmp/daemon.sock' }; beforeEach(async () => { metadataPath = path.join(os.tmpdir(), `mcporter-meta-${randomUUID().slice(0, 8)}.json`); }); afterEach(async () => { await fs.rm(metadataPath, { force: true }).catch(() => {}); }); it('matches when pid and socket agree', async () => { await fs.writeFile(metadataPath, JSON.stringify({ pid: 4321, socketPath: '/tmp/daemon.sock' }), 'utf8'); expect(await metadataMatches(metadataPath, live)).toBe(true); }); it('does not match a different pid', async () => { await fs.writeFile(metadataPath, JSON.stringify({ pid: 9999, socketPath: '/tmp/daemon.sock' }), 'utf8'); expect(await metadataMatches(metadataPath, live)).toBe(false); }); it('does not match when metadata is missing', async () => { expect(await metadataMatches(metadataPath, live)).toBe(false); }); it('does not match when metadata is corrupt', async () => { await fs.writeFile(metadataPath, '{ not json', 'utf8'); expect(await metadataMatches(metadataPath, live)).toBe(false); }); }); describe('daemon artifact cleanup', () => { let dir: string; let metadataPath: string; let socketPath: string; beforeEach(async () => { dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-cleanup-')); metadataPath = path.join(dir, 'daemon.json'); socketPath = path.join(dir, 'daemon.sock'); await fs.writeFile(socketPath, 'socket', 'utf8'); }); afterEach(async () => { await fs.rm(dir, { recursive: true, force: true }); }); it('removes artifacts still owned by the stopping daemon', async () => { await fs.writeFile(metadataPath, JSON.stringify({ pid: 4321, socketPath }), 'utf8'); await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321); await expect(fs.access(metadataPath)).rejects.toThrow(); await expect(fs.access(socketPath)).rejects.toThrow(); }); it('preserves artifacts replaced by a newer daemon', async () => { await fs.writeFile(metadataPath, JSON.stringify({ pid: 9876, socketPath }), 'utf8'); await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321); await expect(fs.access(metadataPath)).resolves.toBeUndefined(); await expect(fs.access(socketPath)).resolves.toBeUndefined(); }); }); function createRuntimeDouble(): Pick { return { callTool: vi.fn().mockResolvedValue({ ok: true }), listTools: vi.fn().mockResolvedValue([]), }; } function createManagedServers(): Map { return new Map([ [ 'local', { name: 'local', command: { kind: 'stdio', command: 'node', args: ['server.js'], cwd: '/tmp' }, lifecycle: { mode: 'keep-alive' }, }, ], [ 'oauth', { name: 'oauth', command: { kind: 'http', url: new URL('https://oauth.example.com/mcp') }, lifecycle: { mode: 'keep-alive' }, }, ], ]); } function statusServer(result: Record): net.Server { return net.createServer((socket) => { socket.on('data', () => socket.end(JSON.stringify({ id: '1', ok: true, result }))); }); }