| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- /**
- * 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 小时
- });
- });
|