McpOrchestrator interface with DockerContainerManager implementation, instance service for lifecycle management, instance API routes, and docker-compose with mcpd service. 127 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|