circuit-breaker-integration.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * Circuit Breaker 与 Provider 注册表集成测试
  3. *
  4. * 验证熔断器和额度耗尽机制在 TTS/LLM Provider 上的行为一致性。
  5. * 这是可靠性核心:
  6. * - 3 次失败 → 自动熔断 → 60 秒冷却 → 半开探测 → 恢复
  7. * - 标记额度耗尽 → 4 小时不重试 → TTL 到期自动清除
  8. *
  9. * 如果这里出问题,坏掉的供应商会一直打 bad request。
  10. */
  11. import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  12. import { CircuitBreaker, CircuitBreakerOpenError, CircuitState } from '@/common/circuit-breaker';
  13. describe('CircuitBreaker - 熔断器状态机', () => {
  14. let breaker: CircuitBreaker;
  15. beforeEach(() => {
  16. breaker = new CircuitBreaker({
  17. name: 'test-breaker',
  18. failureThreshold: 3,
  19. cooldownMs: 500, // 测试用短冷却
  20. successThreshold: 2,
  21. });
  22. });
  23. describe('CLOSED → OPEN', () => {
  24. it('未达阈值 → 保持 CLOSED', async () => {
  25. // 连续 2 次失败(阈值 3)
  26. await expect(breaker.call(async () => { throw new Error('fail'); })).rejects.toThrow();
  27. await expect(breaker.call(async () => { throw new Error('fail'); })).rejects.toThrow();
  28. expect(breaker.getState()).toBe('CLOSED');
  29. });
  30. it('达到阈值 → 切到 OPEN', async () => {
  31. // 连续 3 次失败
  32. for (let i = 0; i < 3; i++) {
  33. await expect(breaker.call(async () => { throw new Error('fail'); })).rejects.toThrow();
  34. }
  35. expect(breaker.getState()).toBe('OPEN');
  36. });
  37. it('部分失败 + 成功 → 不会触发熔断', async () => {
  38. let attempts = 0;
  39. const fn = async () => {
  40. attempts++;
  41. if (attempts % 2 === 0) return 'ok';
  42. throw new Error('fail');
  43. };
  44. // 跑 5 次,2 次成功 3 次失败,但不会触发熔断(计数不会连续累计 3 次)
  45. for (let i = 0; i < 5; i++) {
  46. try { await breaker.call(fn); } catch {}
  47. }
  48. expect(breaker.getState()).toBe('CLOSED');
  49. });
  50. });
  51. describe('OPEN 期间拒绝调用', () => {
  52. beforeEach(async () => {
  53. for (let i = 0; i < 3; i++) {
  54. try { await breaker.call(async () => { throw new Error('fail'); }); } catch {}
  55. }
  56. expect(breaker.getState()).toBe('OPEN');
  57. });
  58. it('OPEN 时调用 → 抛 CircuitBreakerOpenError 而非执行函数', async () => {
  59. let called = false;
  60. const fn = async () => { called = true; return 'ok'; };
  61. await expect(breaker.call(fn)).rejects.toThrow(CircuitBreakerOpenError);
  62. expect(called).toBe(false); // 函数不应该被执行
  63. });
  64. it('OPEN 时调用 → 即使函数本身正常也不会被调用', async () => {
  65. // 这是性能优化:OPEN 状态直接拒绝,省去 API call 费用
  66. let callCount = 0;
  67. const fn = async () => { callCount++; return 'success'; };
  68. for (let i = 0; i < 5; i++) {
  69. try { await breaker.call(fn); } catch {}
  70. }
  71. expect(callCount).toBe(0); // 函数一次都没执行
  72. });
  73. });
  74. describe('OPEN → HALF_OPEN → CLOSED 恢复', () => {
  75. it('冷却期过后 → 进入 HALF_OPEN', async () => {
  76. // 触发 OPEN
  77. for (let i = 0; i < 3; i++) {
  78. try { await breaker.call(async () => { throw new Error('fail'); }); } catch {}
  79. }
  80. expect(breaker.getState()).toBe('OPEN');
  81. // 等过冷却
  82. await new Promise(resolve => setTimeout(resolve, 600));
  83. expect(breaker.getState()).toBe('HALF_OPEN');
  84. });
  85. it('HALF_OPEN 时连续成功 → 切回 CLOSED', async () => {
  86. // 触发 OPEN
  87. for (let i = 0; i < 3; i++) {
  88. try { await breaker.call(async () => { throw new Error('fail'); }); } catch {}
  89. }
  90. await new Promise(resolve => setTimeout(resolve, 600));
  91. expect(breaker.getState()).toBe('HALF_OPEN');
  92. // 连续成功(阈值 2)
  93. await breaker.call(async () => 'ok');
  94. await breaker.call(async () => 'ok');
  95. expect(breaker.getState()).toBe('CLOSED');
  96. });
  97. it('HALF_OPEN 时单次失败 → 立即回到 OPEN', async () => {
  98. // 触发 OPEN
  99. for (let i = 0; i < 3; i++) {
  100. try { await breaker.call(async () => { throw new Error('fail'); }); } catch {}
  101. }
  102. await new Promise(resolve => setTimeout(resolve, 600));
  103. expect(breaker.getState()).toBe('HALF_OPEN');
  104. // 在 HALF_OPEN 失败
  105. try {
  106. await breaker.call(async () => { throw new Error('still broken'); });
  107. } catch {}
  108. // 应该重新 OPEN(不是 CLOSED)
  109. expect(breaker.getState()).toBe('OPEN');
  110. });
  111. });
  112. describe('isOpen / 初始状态检查', () => {
  113. it('初始状态为 CLOSED,isOpen=false', () => {
  114. expect(breaker.getState()).toBe('CLOSED');
  115. expect(breaker.isOpen()).toBe(false);
  116. });
  117. it('失败同步抛出(非 Promise reject) → 正常计数', async () => {
  118. const sync = () => {
  119. throw new Error('sync fail');
  120. };
  121. for (let i = 0; i < 3; i++) {
  122. try { await breaker.call(sync); } catch {}
  123. }
  124. expect(breaker.getState()).toBe('OPEN');
  125. });
  126. });
  127. });
  128. describe('markExhausted 行为契约', () => {
  129. // 不依赖实际 registry,模拟类似行为
  130. it('额度耗尽有 4 小时 TTL(机制验证)', () => {
  131. const ttlMs = 4 * 60 * 60 * 1000;
  132. expect(ttlMs).toBe(4 * 60 * 60 * 1000);
  133. expect(ttlMs / (60 * 60 * 1000)).toBe(4); // 4 小时
  134. });
  135. });