305 lines
8.8 KiB
TypeScript
305 lines
8.8 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import { TieredHealthMonitor } from '../src/health/tiered.js';
|
||
|
|
import type { TieredHealthMonitorDeps } from '../src/health/tiered.js';
|
||
|
|
import type { McpdClient } from '../src/http/mcpd-client.js';
|
||
|
|
import { ProviderRegistry } from '../src/providers/registry.js';
|
||
|
|
import type { LlmProvider } from '../src/providers/types.js';
|
||
|
|
|
||
|
|
function mockMcpdClient(overrides?: {
|
||
|
|
getResult?: unknown;
|
||
|
|
getFails?: boolean;
|
||
|
|
instancesResult?: { instances: Array<{ name: string; status: string }> };
|
||
|
|
instancesFails?: boolean;
|
||
|
|
}): McpdClient {
|
||
|
|
const client = {
|
||
|
|
get: vi.fn(async (path: string) => {
|
||
|
|
if (path === '/health') {
|
||
|
|
if (overrides?.getFails) {
|
||
|
|
throw new Error('Connection refused');
|
||
|
|
}
|
||
|
|
return overrides?.getResult ?? { status: 'ok' };
|
||
|
|
}
|
||
|
|
if (path === '/instances') {
|
||
|
|
if (overrides?.instancesFails) {
|
||
|
|
throw new Error('Connection refused');
|
||
|
|
}
|
||
|
|
return overrides?.instancesResult ?? { instances: [] };
|
||
|
|
}
|
||
|
|
return {};
|
||
|
|
}),
|
||
|
|
post: vi.fn(),
|
||
|
|
put: vi.fn(),
|
||
|
|
delete: vi.fn(),
|
||
|
|
forward: vi.fn(),
|
||
|
|
} as unknown as McpdClient;
|
||
|
|
return client;
|
||
|
|
}
|
||
|
|
|
||
|
|
function mockLlmProvider(name: string): LlmProvider {
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
complete: vi.fn(),
|
||
|
|
listModels: vi.fn(async () => []),
|
||
|
|
isAvailable: vi.fn(async () => true),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('TieredHealthMonitor', () => {
|
||
|
|
let providerRegistry: ProviderRegistry;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
providerRegistry = new ProviderRegistry();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('mcplocal health', () => {
|
||
|
|
it('reports healthy status with uptime', async () => {
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcplocal.status).toBe('healthy');
|
||
|
|
expect(result.mcplocal.uptime).toBeGreaterThanOrEqual(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports null llmProvider when none registered', async () => {
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcplocal.llmProvider).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports active llmProvider name when one is registered', async () => {
|
||
|
|
const provider = mockLlmProvider('openai');
|
||
|
|
providerRegistry.register(provider);
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcplocal.llmProvider).toBe('openai');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports the currently active provider when multiple registered', async () => {
|
||
|
|
providerRegistry.register(mockLlmProvider('openai'));
|
||
|
|
providerRegistry.register(mockLlmProvider('anthropic'));
|
||
|
|
providerRegistry.setActive('anthropic');
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcplocal.llmProvider).toBe('anthropic');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('mcpd health', () => {
|
||
|
|
it('reports connected when mcpd /health responds successfully', async () => {
|
||
|
|
const client = mockMcpdClient();
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcpd.status).toBe('connected');
|
||
|
|
expect(result.mcpd.url).toBe('http://localhost:3100');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports disconnected when mcpd /health throws', async () => {
|
||
|
|
const client = mockMcpdClient({ getFails: true });
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcpd.status).toBe('disconnected');
|
||
|
|
expect(result.mcpd.url).toBe('http://localhost:3100');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports disconnected when mcpdClient is null', async () => {
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcpd.status).toBe('disconnected');
|
||
|
|
expect(result.mcpd.url).toBe('http://localhost:3100');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes the configured mcpd URL in the response', async () => {
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://custom-host:9999',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcpd.url).toBe('http://custom-host:9999');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('instances', () => {
|
||
|
|
it('returns instances from mcpd /instances endpoint', async () => {
|
||
|
|
const client = mockMcpdClient({
|
||
|
|
instancesResult: {
|
||
|
|
instances: [
|
||
|
|
{ name: 'slack', status: 'running' },
|
||
|
|
{ name: 'github', status: 'stopped' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.instances).toHaveLength(2);
|
||
|
|
expect(result.instances[0]).toEqual({ name: 'slack', status: 'running' });
|
||
|
|
expect(result.instances[1]).toEqual({ name: 'github', status: 'stopped' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty array when mcpdClient is null', async () => {
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.instances).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty array when /instances request fails', async () => {
|
||
|
|
const client = mockMcpdClient({ instancesFails: true });
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.instances).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty array when mcpd has no instances', async () => {
|
||
|
|
const client = mockMcpdClient({
|
||
|
|
instancesResult: { instances: [] },
|
||
|
|
});
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.instances).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('full integration', () => {
|
||
|
|
it('returns complete tiered status with all sections', async () => {
|
||
|
|
providerRegistry.register(mockLlmProvider('openai'));
|
||
|
|
|
||
|
|
const client = mockMcpdClient({
|
||
|
|
instancesResult: {
|
||
|
|
instances: [
|
||
|
|
{ name: 'slack', status: 'running' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
// Verify structure
|
||
|
|
expect(result).toHaveProperty('mcplocal');
|
||
|
|
expect(result).toHaveProperty('mcpd');
|
||
|
|
expect(result).toHaveProperty('instances');
|
||
|
|
|
||
|
|
// mcplocal
|
||
|
|
expect(result.mcplocal.status).toBe('healthy');
|
||
|
|
expect(typeof result.mcplocal.uptime).toBe('number');
|
||
|
|
expect(result.mcplocal.llmProvider).toBe('openai');
|
||
|
|
|
||
|
|
// mcpd
|
||
|
|
expect(result.mcpd.status).toBe('connected');
|
||
|
|
|
||
|
|
// instances
|
||
|
|
expect(result.instances).toHaveLength(1);
|
||
|
|
expect(result.instances[0]?.name).toBe('slack');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles degraded scenario: no mcpd, no provider', async () => {
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: null,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcplocal.status).toBe('healthy');
|
||
|
|
expect(result.mcplocal.llmProvider).toBeNull();
|
||
|
|
expect(result.mcpd.status).toBe('disconnected');
|
||
|
|
expect(result.instances).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles mcpd connected but instances endpoint failing', async () => {
|
||
|
|
const client = mockMcpdClient({ instancesFails: true });
|
||
|
|
|
||
|
|
const monitor = new TieredHealthMonitor({
|
||
|
|
mcpdClient: client,
|
||
|
|
providerRegistry,
|
||
|
|
mcpdUrl: 'http://localhost:3100',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await monitor.checkHealth();
|
||
|
|
|
||
|
|
expect(result.mcpd.status).toBe('connected');
|
||
|
|
expect(result.instances).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|