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 => ({ 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']); }); });