feat: implement v2 3-tier architecture (mcpctl → mcplocal → mcpd)
- Rename local-proxy to mcplocal with HTTP server, LLM pipeline, mcpd discovery - Add LLM pre-processing: token estimation, filter cache, metrics, Gemini CLI + DeepSeek providers - Add mcpd auth (login/logout) and MCP proxy endpoints - Update CLI: dual URLs (mcplocalUrl/mcpdUrl), auth commands, --direct flag - Add tiered health monitoring, shell completions, e2e integration tests - 57 test files, 597 tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
304
src/mcplocal/tests/tiered-health.test.ts
Normal file
304
src/mcplocal/tests/tiered-health.test.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user