provider-registry.test.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /**
  2. * ProviderRegistry 单元测试
  3. * 测试供应商注册、查找、轮转、熔断集成
  4. */
  5. import { describe, it, expect, beforeEach, vi } from 'vitest';
  6. import { ProviderRegistry, ProviderNode } from '@/common/provider-registry';
  7. import { CircuitBreaker } from '@/common/circuit-breaker';
  8. interface MockProvider {
  9. readonly name: string;
  10. readonly vendor: string;
  11. }
  12. const createMockProvider = (name: string, vendor: string = 'mock'): MockProvider => ({
  13. name,
  14. vendor,
  15. });
  16. describe('ProviderRegistry', () => {
  17. let registry: ProviderRegistry<MockProvider>;
  18. beforeEach(() => {
  19. registry = new ProviderRegistry<MockProvider>();
  20. });
  21. describe('注册与注销', () => {
  22. it('注册一个 provider', () => {
  23. const provider = createMockProvider('provider-a');
  24. registry.register(provider);
  25. expect(registry.get('provider-a')).toBeDefined();
  26. expect(registry.get('provider-a')?.provider.name).toBe('provider-a');
  27. });
  28. it('注册多个 provider', () => {
  29. registry.register(createMockProvider('a'));
  30. registry.register(createMockProvider('b'));
  31. registry.register(createMockProvider('c'));
  32. expect(registry.size).toBe(3);
  33. });
  34. it('同名 provider 覆盖', () => {
  35. registry.register(createMockProvider('same'));
  36. registry.register(createMockProvider('same'));
  37. expect(registry.size).toBe(1);
  38. });
  39. it('注销已存在的 provider', () => {
  40. registry.register(createMockProvider('to-remove'));
  41. expect(registry.get('to-remove')).toBeDefined();
  42. const result = registry.unregister('to-remove');
  43. expect(result).toBe(true);
  44. expect(registry.get('to-remove')).toBeUndefined();
  45. });
  46. it('注销不存在的 provider 返回 false', () => {
  47. const result = registry.unregister('nonexistent');
  48. expect(result).toBe(false);
  49. });
  50. });
  51. describe('查找', () => {
  52. beforeEach(() => {
  53. registry.register(createMockProvider('provider-a', 'vendor-x'));
  54. registry.register(createMockProvider('provider-b', 'vendor-x'));
  55. registry.register(createMockProvider('provider-c', 'vendor-y'));
  56. });
  57. it('按名称查找', () => {
  58. const node = registry.get('provider-a');
  59. expect(node?.provider.name).toBe('provider-a');
  60. });
  61. it('按供应商查找', () => {
  62. const node = registry.getByVendor('vendor-x');
  63. expect(node?.provider.name).toBe('provider-a');
  64. });
  65. it('获取所有节点', () => {
  66. const all = registry.list();
  67. expect(all.length).toBe(3);
  68. });
  69. it('过滤器筛选', () => {
  70. const enabledOnly = registry.list(n => n.enabled);
  71. expect(enabledOnly.length).toBe(3);
  72. });
  73. });
  74. describe('状态查询', () => {
  75. beforeEach(() => {
  76. registry.register(createMockProvider('enabled-provider'));
  77. registry.register(createMockProvider('disabled-provider'), { enabled: false });
  78. });
  79. it('listEnabled 返回已启用节点', () => {
  80. const enabled = registry.listEnabled();
  81. expect(enabled.length).toBe(1);
  82. expect(enabled[0].provider.name).toBe('enabled-provider');
  83. });
  84. it('listAvailable 返回可用节点(启用+未熔断+未耗尽)', () => {
  85. const available = registry.listAvailable();
  86. expect(available.length).toBe(1);
  87. expect(available[0].provider.name).toBe('enabled-provider');
  88. });
  89. it('listBroken 返回已熔断节点', async () => {
  90. const broken = registry.listBroken();
  91. expect(broken.length).toBe(0);
  92. // 触发熔断
  93. const node = registry.get('enabled-provider');
  94. for (let i = 0; i < 3; i++) {
  95. await node?.breaker.call(() => Promise.reject(new Error('fail'))).catch(() => {});
  96. }
  97. // 确保状态更新
  98. await new Promise(resolve => setTimeout(resolve, 50));
  99. const brokenAfter = registry.listBroken();
  100. expect(brokenAfter.length).toBe(1);
  101. });
  102. it('listExhausted 返回额度耗尽节点', () => {
  103. registry.markExhausted('enabled-provider', 'quota exceeded', 0);
  104. const exhausted = registry.listExhausted();
  105. expect(exhausted.length).toBe(1);
  106. expect(exhausted[0].provider.name).toBe('enabled-provider');
  107. });
  108. });
  109. describe('额度耗尽管理', () => {
  110. beforeEach(() => {
  111. registry.register(createMockProvider('provider'));
  112. });
  113. it('markExhausted 标记供应商额度耗尽', () => {
  114. registry.markExhausted('provider', 'Daily quota exceeded', 0);
  115. const node = registry.get('provider');
  116. expect(node?.exhausted).toBe(true);
  117. expect(node?.exhaustedReason).toBe('Daily quota exceeded');
  118. });
  119. it('markExhausted 带 TTL 自动恢复', async () => {
  120. registry.markExhausted('provider', 'temp', 100);
  121. const node = registry.get('provider');
  122. expect(node?.exhausted).toBe(true);
  123. await new Promise(resolve => setTimeout(resolve, 150));
  124. expect(node?.exhausted).toBe(false);
  125. });
  126. it('clearExhausted 清除耗尽标记', () => {
  127. registry.markExhausted('provider', 'quota', 0);
  128. expect(registry.get('provider')?.exhausted).toBe(true);
  129. const result = registry.clearExhausted('provider');
  130. expect(result).toBe(true);
  131. expect(registry.get('provider')?.exhausted).toBe(false);
  132. });
  133. it('clearAllExhausted 清除所有耗尽标记', () => {
  134. registry.register(createMockProvider('p1'));
  135. registry.register(createMockProvider('p2'));
  136. registry.markExhausted('p1', 'q1', 0);
  137. registry.markExhausted('p2', 'q2', 0);
  138. registry.clearAllExhausted();
  139. expect(registry.listExhausted().length).toBe(0);
  140. });
  141. });
  142. describe('轮转调度', () => {
  143. beforeEach(() => {
  144. registry.register(createMockProvider('p1'));
  145. registry.register(createMockProvider('p2'));
  146. registry.register(createMockProvider('p3'));
  147. });
  148. it('getNextAvailable 返回下一个可用 provider', () => {
  149. const next = registry.getNextAvailable('p1');
  150. expect(next?.provider.name).toBe('p2');
  151. });
  152. it('轮转到末尾后从头开始', () => {
  153. const next = registry.getNextAvailable('p3');
  154. expect(next?.provider.name).toBe('p1');
  155. });
  156. it('当前 provider 不可用时返回第一个可用', () => {
  157. // 禁用 p1
  158. const p1Node = registry.get('p1');
  159. p1Node && (p1Node.enabled = false);
  160. const next = registry.getNextAvailable('p1');
  161. expect(next?.provider.name).toBe('p2');
  162. });
  163. it('所有 provider 都不可用时返回 undefined', () => {
  164. registry.clearAllExhausted();
  165. registry.get('p1')!.enabled = false;
  166. registry.get('p2')!.enabled = false;
  167. registry.get('p3')!.enabled = false;
  168. const next = registry.getNextAvailable('p1');
  169. expect(next).toBeUndefined();
  170. });
  171. it('getPrimary 返回第一个可用 provider', () => {
  172. const primary = registry.getPrimary();
  173. expect(primary?.provider.name).toBe('p1');
  174. });
  175. });
  176. describe('熔断器集成', () => {
  177. beforeEach(() => {
  178. registry.register(createMockProvider('provider'), {
  179. breakerConfig: { failureThreshold: 3, cooldownMs: 1000 }
  180. });
  181. });
  182. it('Provider 节点包含熔断器', () => {
  183. const node = registry.get('provider');
  184. expect(node?.breaker).toBeInstanceOf(CircuitBreaker);
  185. });
  186. it('熔断后 provider 不再可用', async () => {
  187. const node = registry.get('provider');
  188. // 触发熔断
  189. for (let i = 0; i < 3; i++) {
  190. await node?.breaker.call(() => Promise.reject(new Error('fail'))).catch(() => {});
  191. }
  192. expect(node?.breaker.isOpen()).toBe(true);
  193. expect(registry.listAvailable().length).toBe(0);
  194. });
  195. it('resetAllBreakers 重置所有熔断器', async () => {
  196. const node = registry.get('provider');
  197. for (let i = 0; i < 3; i++) {
  198. await node?.breaker.call(() => Promise.reject(new Error('fail'))).catch(() => {});
  199. }
  200. registry.resetAllBreakers();
  201. expect(node?.breaker.isOpen()).toBe(false);
  202. expect(registry.listAvailable().length).toBe(1);
  203. });
  204. });
  205. describe('健康检查', () => {
  206. beforeEach(() => {
  207. registry.register(createMockProvider('healthy'));
  208. registry.register(createMockProvider('unhealthy'));
  209. });
  210. it('healthCheckAll 检查所有 provider', async () => {
  211. const results = await registry.healthCheckAll(async (p) => {
  212. return p.name === 'healthy';
  213. });
  214. expect(results.get('healthy')).toBe(true);
  215. expect(results.get('unhealthy')).toBe(false);
  216. });
  217. it('健康检查失败触发熔断', async () => {
  218. const node = registry.get('healthy')!;
  219. await registry.healthCheckAll(async (p) => {
  220. if (p.name === 'healthy') {
  221. throw new Error('health check failed');
  222. }
  223. return true;
  224. });
  225. // 健康检查失败应该触发熔断
  226. expect(node.breaker.isOpen()).toBe(false); // 注意:healthCheckAll 内部不会自动熔断
  227. });
  228. });
  229. describe('状态摘要', () => {
  230. beforeEach(() => {
  231. registry.register(createMockProvider('p1', 'vendor1'));
  232. registry.register(createMockProvider('p2', 'vendor2'));
  233. });
  234. it('getStatus 返回完整状态', () => {
  235. const status = registry.getStatus();
  236. expect(status.length).toBe(2);
  237. expect(status[0]).toHaveProperty('name');
  238. expect(status[0]).toHaveProperty('vendor');
  239. expect(status[0]).toHaveProperty('enabled');
  240. expect(status[0]).toHaveProperty('exhausted');
  241. expect(status[0]).toHaveProperty('state');
  242. });
  243. });
  244. });