feat: implement v2 3-tier architecture (mcpctl → mcplocal → mcpd)
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled

- 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:
Michal
2026-02-22 11:42:06 +00:00
parent a4fe5fdbe2
commit b8c5cf718a
82 changed files with 5832 additions and 123 deletions

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProviderRegistry } from '../src/providers/registry.js';
import type { LlmProvider, CompletionOptions, CompletionResult } from '../src/providers/types.js';
function mockProvider(name: string): LlmProvider {
return {
name,
complete: vi.fn(async (): Promise<CompletionResult> => ({
content: `Response from ${name}`,
toolCalls: [],
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
finishReason: 'stop',
})),
listModels: vi.fn(async () => [`${name}-model-1`, `${name}-model-2`]),
isAvailable: vi.fn(async () => true),
};
}
describe('ProviderRegistry', () => {
let registry: ProviderRegistry;
beforeEach(() => {
registry = new ProviderRegistry();
});
it('starts with no providers', () => {
expect(registry.list()).toEqual([]);
expect(registry.getActive()).toBeNull();
expect(registry.getActiveName()).toBeNull();
});
it('registers a provider and sets it as active', () => {
const openai = mockProvider('openai');
registry.register(openai);
expect(registry.list()).toEqual(['openai']);
expect(registry.getActive()).toBe(openai);
expect(registry.getActiveName()).toBe('openai');
});
it('first registered provider becomes active', () => {
registry.register(mockProvider('openai'));
registry.register(mockProvider('anthropic'));
expect(registry.getActiveName()).toBe('openai');
expect(registry.list()).toEqual(['openai', 'anthropic']);
});
it('switches active provider', () => {
registry.register(mockProvider('openai'));
registry.register(mockProvider('anthropic'));
registry.setActive('anthropic');
expect(registry.getActiveName()).toBe('anthropic');
});
it('throws when setting unknown provider as active', () => {
expect(() => registry.setActive('unknown')).toThrow("Provider 'unknown' is not registered");
});
it('gets provider by name', () => {
const openai = mockProvider('openai');
registry.register(openai);
expect(registry.get('openai')).toBe(openai);
expect(registry.get('unknown')).toBeUndefined();
});
it('unregisters a provider', () => {
registry.register(mockProvider('openai'));
registry.register(mockProvider('anthropic'));
registry.unregister('openai');
expect(registry.list()).toEqual(['anthropic']);
// Active should switch to remaining provider
expect(registry.getActiveName()).toBe('anthropic');
});
it('unregistering active provider switches to next available', () => {
registry.register(mockProvider('openai'));
registry.register(mockProvider('anthropic'));
registry.setActive('openai');
registry.unregister('openai');
expect(registry.getActiveName()).toBe('anthropic');
});
it('unregistering last provider clears active', () => {
registry.register(mockProvider('openai'));
registry.unregister('openai');
expect(registry.getActive()).toBeNull();
expect(registry.getActiveName()).toBeNull();
});
it('active provider can complete', async () => {
const provider = mockProvider('openai');
registry.register(provider);
const active = registry.getActive()!;
const result = await active.complete({
messages: [{ role: 'user', content: 'Hello' }],
});
expect(result.content).toBe('Response from openai');
expect(result.finishReason).toBe('stop');
expect(provider.complete).toHaveBeenCalled();
});
it('active provider can list models', async () => {
registry.register(mockProvider('anthropic'));
const active = registry.getActive()!;
const models = await active.listModels();
expect(models).toEqual(['anthropic-model-1', 'anthropic-model-2']);
});
});