import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DockerContainerManager } from '../src/services/docker/container-manager.js'; import type { ContainerSpec } from '../src/services/orchestrator.js'; // Mock dockerode vi.mock('dockerode', () => { const mockContainer = { id: 'ctr-abc123', start: vi.fn(async () => {}), stop: vi.fn(async () => {}), remove: vi.fn(async () => {}), inspect: vi.fn(async () => ({ Id: 'ctr-abc123', Name: '/mcpctl-test', State: { Status: 'running' }, Created: '2024-01-01T00:00:00Z', NetworkSettings: { Ports: { '3000/tcp': [{ HostIp: '0.0.0.0', HostPort: '32768' }], }, }, })), logs: vi.fn(async () => Buffer.from('test log output')), }; const mockModem = { followProgress: vi.fn((_stream: unknown, onFinished: (err: Error | null) => void) => { onFinished(null); }), }; class MockDocker { modem = mockModem; ping = vi.fn(async () => 'OK'); pull = vi.fn(async () => ({})); createContainer = vi.fn(async () => mockContainer); getContainer = vi.fn(() => mockContainer); } return { default: MockDocker }; }); describe('DockerContainerManager', () => { let manager: DockerContainerManager; beforeEach(() => { vi.clearAllMocks(); manager = new DockerContainerManager(); }); describe('ping', () => { it('returns true when Docker is available', async () => { expect(await manager.ping()).toBe(true); }); }); describe('pullImage', () => { it('pulls an image', async () => { await manager.pullImage('node:20-alpine'); // No error = success }); }); describe('createContainer', () => { it('creates and starts a container', async () => { const spec: ContainerSpec = { image: 'ghcr.io/slack-mcp:latest', name: 'mcpctl-slack-inst1', env: { SLACK_TOKEN: 'xoxb-test' }, containerPort: 3000, hostPort: null, labels: { 'mcpctl.server-id': 'srv-1' }, }; const result = await manager.createContainer(spec); expect(result.containerId).toBe('ctr-abc123'); expect(result.name).toBe('mcpctl-test'); expect(result.state).toBe('running'); expect(result.port).toBe(32768); }); it('applies resource limits', async () => { const spec: ContainerSpec = { image: 'test:latest', name: 'test-container', memoryLimit: 256 * 1024 * 1024, nanoCpus: 250_000_000, }; await manager.createContainer(spec); // The mock captures the call - we verify it doesn't throw }); }); describe('stopContainer', () => { it('stops a container', async () => { await manager.stopContainer('ctr-abc123'); // No error = success }); }); describe('removeContainer', () => { it('removes a container', async () => { await manager.removeContainer('ctr-abc123', true); // No error = success }); }); describe('inspectContainer', () => { it('returns container info with mapped state', async () => { const info = await manager.inspectContainer('ctr-abc123'); expect(info.containerId).toBe('ctr-abc123'); expect(info.state).toBe('running'); expect(info.port).toBe(32768); }); }); describe('getContainerLogs', () => { it('returns container logs', async () => { const logs = await manager.getContainerLogs('ctr-abc123', { tail: 50 }); expect(logs.stdout).toBe('test log output'); }); }); });