/** * ProviderRegistry 单元测试 * 测试供应商注册、查找、轮转、熔断集成 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ProviderRegistry, ProviderNode } from '@/common/provider-registry'; import { CircuitBreaker } from '@/common/circuit-breaker'; interface MockProvider { readonly name: string; readonly vendor: string; } const createMockProvider = (name: string, vendor: string = 'mock'): MockProvider => ({ name, vendor, }); describe('ProviderRegistry', () => { let registry: ProviderRegistry; beforeEach(() => { registry = new ProviderRegistry(); }); describe('注册与注销', () => { it('注册一个 provider', () => { const provider = createMockProvider('provider-a'); registry.register(provider); expect(registry.get('provider-a')).toBeDefined(); expect(registry.get('provider-a')?.provider.name).toBe('provider-a'); }); it('注册多个 provider', () => { registry.register(createMockProvider('a')); registry.register(createMockProvider('b')); registry.register(createMockProvider('c')); expect(registry.size).toBe(3); }); it('同名 provider 覆盖', () => { registry.register(createMockProvider('same')); registry.register(createMockProvider('same')); expect(registry.size).toBe(1); }); it('注销已存在的 provider', () => { registry.register(createMockProvider('to-remove')); expect(registry.get('to-remove')).toBeDefined(); const result = registry.unregister('to-remove'); expect(result).toBe(true); expect(registry.get('to-remove')).toBeUndefined(); }); it('注销不存在的 provider 返回 false', () => { const result = registry.unregister('nonexistent'); expect(result).toBe(false); }); }); describe('查找', () => { beforeEach(() => { registry.register(createMockProvider('provider-a', 'vendor-x')); registry.register(createMockProvider('provider-b', 'vendor-x')); registry.register(createMockProvider('provider-c', 'vendor-y')); }); it('按名称查找', () => { const node = registry.get('provider-a'); expect(node?.provider.name).toBe('provider-a'); }); it('按供应商查找', () => { const node = registry.getByVendor('vendor-x'); expect(node?.provider.name).toBe('provider-a'); }); it('获取所有节点', () => { const all = registry.list(); expect(all.length).toBe(3); }); it('过滤器筛选', () => { const enabledOnly = registry.list(n => n.enabled); expect(enabledOnly.length).toBe(3); }); }); describe('状态查询', () => { beforeEach(() => { registry.register(createMockProvider('enabled-provider')); registry.register(createMockProvider('disabled-provider'), { enabled: false }); }); it('listEnabled 返回已启用节点', () => { const enabled = registry.listEnabled(); expect(enabled.length).toBe(1); expect(enabled[0].provider.name).toBe('enabled-provider'); }); it('listAvailable 返回可用节点(启用+未熔断+未耗尽)', () => { const available = registry.listAvailable(); expect(available.length).toBe(1); expect(available[0].provider.name).toBe('enabled-provider'); }); it('listBroken 返回已熔断节点', async () => { const broken = registry.listBroken(); expect(broken.length).toBe(0); // 触发熔断 const node = registry.get('enabled-provider'); for (let i = 0; i < 3; i++) { await node?.breaker.call(() => Promise.reject(new Error('fail'))).catch(() => {}); } // 确保状态更新 await new Promise(resolve => setTimeout(resolve, 50)); const brokenAfter = registry.listBroken(); expect(brokenAfter.length).toBe(1); }); it('listExhausted 返回额度耗尽节点', () => { registry.markExhausted('enabled-provider', 'quota exceeded', 0); const exhausted = registry.listExhausted(); expect(exhausted.length).toBe(1); expect(exhausted[0].provider.name).toBe('enabled-provider'); }); }); describe('额度耗尽管理', () => { beforeEach(() => { registry.register(createMockProvider('provider')); }); it('markExhausted 标记供应商额度耗尽', () => { registry.markExhausted('provider', 'Daily quota exceeded', 0); const node = registry.get('provider'); expect(node?.exhausted).toBe(true); expect(node?.exhaustedReason).toBe('Daily quota exceeded'); }); it('markExhausted 带 TTL 自动恢复', async () => { registry.markExhausted('provider', 'temp', 100); const node = registry.get('provider'); expect(node?.exhausted).toBe(true); await new Promise(resolve => setTimeout(resolve, 150)); expect(node?.exhausted).toBe(false); }); it('clearExhausted 清除耗尽标记', () => { registry.markExhausted('provider', 'quota', 0); expect(registry.get('provider')?.exhausted).toBe(true); const result = registry.clearExhausted('provider'); expect(result).toBe(true); expect(registry.get('provider')?.exhausted).toBe(false); }); it('clearAllExhausted 清除所有耗尽标记', () => { registry.register(createMockProvider('p1')); registry.register(createMockProvider('p2')); registry.markExhausted('p1', 'q1', 0); registry.markExhausted('p2', 'q2', 0); registry.clearAllExhausted(); expect(registry.listExhausted().length).toBe(0); }); }); describe('轮转调度', () => { beforeEach(() => { registry.register(createMockProvider('p1')); registry.register(createMockProvider('p2')); registry.register(createMockProvider('p3')); }); it('getNextAvailable 返回下一个可用 provider', () => { const next = registry.getNextAvailable('p1'); expect(next?.provider.name).toBe('p2'); }); it('轮转到末尾后从头开始', () => { const next = registry.getNextAvailable('p3'); expect(next?.provider.name).toBe('p1'); }); it('当前 provider 不可用时返回第一个可用', () => { // 禁用 p1 const p1Node = registry.get('p1'); p1Node && (p1Node.enabled = false); const next = registry.getNextAvailable('p1'); expect(next?.provider.name).toBe('p2'); }); it('所有 provider 都不可用时返回 undefined', () => { registry.clearAllExhausted(); registry.get('p1')!.enabled = false; registry.get('p2')!.enabled = false; registry.get('p3')!.enabled = false; const next = registry.getNextAvailable('p1'); expect(next).toBeUndefined(); }); it('getPrimary 返回第一个可用 provider', () => { const primary = registry.getPrimary(); expect(primary?.provider.name).toBe('p1'); }); }); describe('熔断器集成', () => { beforeEach(() => { registry.register(createMockProvider('provider'), { breakerConfig: { failureThreshold: 3, cooldownMs: 1000 } }); }); it('Provider 节点包含熔断器', () => { const node = registry.get('provider'); expect(node?.breaker).toBeInstanceOf(CircuitBreaker); }); it('熔断后 provider 不再可用', async () => { const node = registry.get('provider'); // 触发熔断 for (let i = 0; i < 3; i++) { await node?.breaker.call(() => Promise.reject(new Error('fail'))).catch(() => {}); } expect(node?.breaker.isOpen()).toBe(true); expect(registry.listAvailable().length).toBe(0); }); it('resetAllBreakers 重置所有熔断器', async () => { const node = registry.get('provider'); for (let i = 0; i < 3; i++) { await node?.breaker.call(() => Promise.reject(new Error('fail'))).catch(() => {}); } registry.resetAllBreakers(); expect(node?.breaker.isOpen()).toBe(false); expect(registry.listAvailable().length).toBe(1); }); }); describe('健康检查', () => { beforeEach(() => { registry.register(createMockProvider('healthy')); registry.register(createMockProvider('unhealthy')); }); it('healthCheckAll 检查所有 provider', async () => { const results = await registry.healthCheckAll(async (p) => { return p.name === 'healthy'; }); expect(results.get('healthy')).toBe(true); expect(results.get('unhealthy')).toBe(false); }); it('健康检查失败触发熔断', async () => { const node = registry.get('healthy')!; await registry.healthCheckAll(async (p) => { if (p.name === 'healthy') { throw new Error('health check failed'); } return true; }); // 健康检查失败应该触发熔断 expect(node.breaker.isOpen()).toBe(false); // 注意:healthCheckAll 内部不会自动熔断 }); }); describe('状态摘要', () => { beforeEach(() => { registry.register(createMockProvider('p1', 'vendor1')); registry.register(createMockProvider('p2', 'vendor2')); }); it('getStatus 返回完整状态', () => { const status = registry.getStatus(); expect(status.length).toBe(2); expect(status[0]).toHaveProperty('name'); expect(status[0]).toHaveProperty('vendor'); expect(status[0]).toHaveProperty('enabled'); expect(status[0]).toHaveProperty('exhausted'); expect(status[0]).toHaveProperty('state'); }); }); });