/** * Circuit Breaker 与 Provider 注册表集成测试 * * 验证熔断器和额度耗尽机制在 TTS/LLM Provider 上的行为一致性。 * 这是可靠性核心: * - 3 次失败 → 自动熔断 → 60 秒冷却 → 半开探测 → 恢复 * - 标记额度耗尽 → 4 小时不重试 → TTL 到期自动清除 * * 如果这里出问题,坏掉的供应商会一直打 bad request。 */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { CircuitBreaker, CircuitBreakerOpenError, CircuitState } from '@/common/circuit-breaker'; describe('CircuitBreaker - 熔断器状态机', () => { let breaker: CircuitBreaker; beforeEach(() => { breaker = new CircuitBreaker({ name: 'test-breaker', failureThreshold: 3, cooldownMs: 500, // 测试用短冷却 successThreshold: 2, }); }); describe('CLOSED → OPEN', () => { it('未达阈值 → 保持 CLOSED', async () => { // 连续 2 次失败(阈值 3) await expect(breaker.call(async () => { throw new Error('fail'); })).rejects.toThrow(); await expect(breaker.call(async () => { throw new Error('fail'); })).rejects.toThrow(); expect(breaker.getState()).toBe('CLOSED'); }); it('达到阈值 → 切到 OPEN', async () => { // 连续 3 次失败 for (let i = 0; i < 3; i++) { await expect(breaker.call(async () => { throw new Error('fail'); })).rejects.toThrow(); } expect(breaker.getState()).toBe('OPEN'); }); it('部分失败 + 成功 → 不会触发熔断', async () => { let attempts = 0; const fn = async () => { attempts++; if (attempts % 2 === 0) return 'ok'; throw new Error('fail'); }; // 跑 5 次,2 次成功 3 次失败,但不会触发熔断(计数不会连续累计 3 次) for (let i = 0; i < 5; i++) { try { await breaker.call(fn); } catch {} } expect(breaker.getState()).toBe('CLOSED'); }); }); describe('OPEN 期间拒绝调用', () => { beforeEach(async () => { for (let i = 0; i < 3; i++) { try { await breaker.call(async () => { throw new Error('fail'); }); } catch {} } expect(breaker.getState()).toBe('OPEN'); }); it('OPEN 时调用 → 抛 CircuitBreakerOpenError 而非执行函数', async () => { let called = false; const fn = async () => { called = true; return 'ok'; }; await expect(breaker.call(fn)).rejects.toThrow(CircuitBreakerOpenError); expect(called).toBe(false); // 函数不应该被执行 }); it('OPEN 时调用 → 即使函数本身正常也不会被调用', async () => { // 这是性能优化:OPEN 状态直接拒绝,省去 API call 费用 let callCount = 0; const fn = async () => { callCount++; return 'success'; }; for (let i = 0; i < 5; i++) { try { await breaker.call(fn); } catch {} } expect(callCount).toBe(0); // 函数一次都没执行 }); }); describe('OPEN → HALF_OPEN → CLOSED 恢复', () => { it('冷却期过后 → 进入 HALF_OPEN', async () => { // 触发 OPEN for (let i = 0; i < 3; i++) { try { await breaker.call(async () => { throw new Error('fail'); }); } catch {} } expect(breaker.getState()).toBe('OPEN'); // 等过冷却 await new Promise(resolve => setTimeout(resolve, 600)); expect(breaker.getState()).toBe('HALF_OPEN'); }); it('HALF_OPEN 时连续成功 → 切回 CLOSED', async () => { // 触发 OPEN for (let i = 0; i < 3; i++) { try { await breaker.call(async () => { throw new Error('fail'); }); } catch {} } await new Promise(resolve => setTimeout(resolve, 600)); expect(breaker.getState()).toBe('HALF_OPEN'); // 连续成功(阈值 2) await breaker.call(async () => 'ok'); await breaker.call(async () => 'ok'); expect(breaker.getState()).toBe('CLOSED'); }); it('HALF_OPEN 时单次失败 → 立即回到 OPEN', async () => { // 触发 OPEN for (let i = 0; i < 3; i++) { try { await breaker.call(async () => { throw new Error('fail'); }); } catch {} } await new Promise(resolve => setTimeout(resolve, 600)); expect(breaker.getState()).toBe('HALF_OPEN'); // 在 HALF_OPEN 失败 try { await breaker.call(async () => { throw new Error('still broken'); }); } catch {} // 应该重新 OPEN(不是 CLOSED) expect(breaker.getState()).toBe('OPEN'); }); }); describe('isOpen / 初始状态检查', () => { it('初始状态为 CLOSED,isOpen=false', () => { expect(breaker.getState()).toBe('CLOSED'); expect(breaker.isOpen()).toBe(false); }); it('失败同步抛出(非 Promise reject) → 正常计数', async () => { const sync = () => { throw new Error('sync fail'); }; for (let i = 0; i < 3; i++) { try { await breaker.call(sync); } catch {} } expect(breaker.getState()).toBe('OPEN'); }); }); }); describe('markExhausted 行为契约', () => { // 不依赖实际 registry,模拟类似行为 it('额度耗尽有 4 小时 TTL(机制验证)', () => { const ttlMs = 4 * 60 * 60 * 1000; expect(ttlMs).toBe(4 * 60 * 60 * 1000); expect(ttlMs / (60 * 60 * 1000)).toBe(4); // 4 小时 }); });