feat: add instance lifecycle management with restart, inspect, and CLI commands
Adds restart/inspect methods to InstanceService, state validation for stop, REST endpoints for restart and inspect, and full CLI command suite for instance list/start/stop/restart/remove/logs/inspect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { InstanceService } from '../src/services/instance.service.js';
|
||||
import { InstanceService, InvalidStateError } from '../src/services/instance.service.js';
|
||||
import { NotFoundError } from '../src/services/mcp-server.service.js';
|
||||
import type { IMcpInstanceRepository, IMcpServerRepository } from '../src/repositories/interfaces.js';
|
||||
import type { McpOrchestrator } from '../src/services/orchestrator.js';
|
||||
@@ -195,6 +195,83 @@ describe('InstanceService', () => {
|
||||
expect(orchestrator.stopContainer).not.toHaveBeenCalled();
|
||||
expect(instanceRepo.updateStatus).toHaveBeenCalledWith('inst-1', 'STOPPED');
|
||||
});
|
||||
|
||||
it('throws InvalidStateError when already stopped', async () => {
|
||||
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||
id: 'inst-1', containerId: 'ctr-abc', status: 'STOPPED',
|
||||
serverId: 'srv-1', port: null, metadata: {},
|
||||
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await expect(service.stop('inst-1')).rejects.toThrow(InvalidStateError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart', () => {
|
||||
it('stops, removes, and starts a new instance', async () => {
|
||||
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||
id: 'inst-1', containerId: 'ctr-abc', status: 'RUNNING',
|
||||
serverId: 'srv-1', port: 3000, metadata: {},
|
||||
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
vi.mocked(serverRepo.findById).mockResolvedValue({
|
||||
id: 'srv-1', name: 'slack', dockerImage: 'slack:latest',
|
||||
packageName: null, transport: 'STDIO', description: '', repositoryUrl: null,
|
||||
envTemplate: [], version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await service.restart('inst-1');
|
||||
|
||||
expect(orchestrator.stopContainer).toHaveBeenCalledWith('ctr-abc');
|
||||
expect(orchestrator.removeContainer).toHaveBeenCalledWith('ctr-abc', true);
|
||||
expect(instanceRepo.delete).toHaveBeenCalledWith('inst-1');
|
||||
expect(instanceRepo.create).toHaveBeenCalled();
|
||||
expect(result.status).toBe('RUNNING');
|
||||
});
|
||||
|
||||
it('handles restart when container already stopped', async () => {
|
||||
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||
id: 'inst-1', containerId: 'ctr-abc', status: 'STOPPED',
|
||||
serverId: 'srv-1', port: null, metadata: {},
|
||||
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
vi.mocked(serverRepo.findById).mockResolvedValue({
|
||||
id: 'srv-1', name: 'slack', dockerImage: 'slack:latest',
|
||||
packageName: null, transport: 'STDIO', description: '', repositoryUrl: null,
|
||||
envTemplate: [], version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await service.restart('inst-1');
|
||||
|
||||
// Should not try to stop an already-stopped container
|
||||
expect(orchestrator.stopContainer).not.toHaveBeenCalled();
|
||||
expect(instanceRepo.delete).toHaveBeenCalledWith('inst-1');
|
||||
expect(result.status).toBe('RUNNING');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspect', () => {
|
||||
it('returns container info', async () => {
|
||||
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||
id: 'inst-1', containerId: 'ctr-abc', status: 'RUNNING',
|
||||
serverId: 'srv-1', port: 3000, metadata: {},
|
||||
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await service.inspect('inst-1');
|
||||
expect(orchestrator.inspectContainer).toHaveBeenCalledWith('ctr-abc');
|
||||
expect(result.containerId).toBe('ctr-abc123');
|
||||
});
|
||||
|
||||
it('throws InvalidStateError when no container', async () => {
|
||||
vi.mocked(instanceRepo.findById).mockResolvedValue({
|
||||
id: 'inst-1', containerId: null, status: 'ERROR',
|
||||
serverId: 'srv-1', port: null, metadata: {},
|
||||
version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await expect(service.inspect('inst-1')).rejects.toThrow(InvalidStateError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
|
||||
Reference in New Issue
Block a user