Files
mcpctl/src/mcpd/tests/container-manager.test.ts

126 lines
3.5 KiB
TypeScript
Raw Normal View History

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');
});
});
});