import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { RetryHandler, CircuitBreaker, CircuitBreakerRegistry, DEFAULT_RETRY_CONFIG } from './RetryHandler'; describe('RetryHandler', () => { let handler: RetryHandler; const fastConfig = { ...DEFAULT_RETRY_CONFIG, baseDelay: 1, maxDelay: 1, backoffMultiplier: 1, }; beforeEach(() => { handler = new RetryHandler(fastConfig); vi.spyOn(Math, 'random').mockReturnValue(0); }); afterEach(() => { vi.restoreAllMocks(); }); describe('execute', () => { it('should execute function successfully without retry', async () => { const fn = vi.fn().mockResolvedValue('success'); const result = await handler.execute(fn); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(1); }); it('should retry on failure and eventually succeed', async () => { const fn = vi.fn() .mockRejectedValueOnce(new Error('First attempt')) .mockResolvedValueOnce('success'); const result = await handler.execute(fn); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(2); }); it('should exhaust retries and throw final error', async () => { const fn = vi.fn().mockRejectedValue(new Error('Always fails')); await expect(handler.execute(fn)).rejects.toThrow('Always fails'); expect(fn).toHaveBeenCalledTimes(fastConfig.maxRetries + 1); }); it('should respect custom retry config', async () => { const customConfig = { ...fastConfig, maxRetries: 2 }; const customHandler = new RetryHandler(customConfig); const fn = vi.fn().mockRejectedValue(new Error('Fail')); await expect(customHandler.execute(fn)).rejects.toThrow('Fail'); expect(fn).toHaveBeenCalledTimes(3); // 2 retries + 1 initial }); }); }); describe('CircuitBreaker', () => { let breaker: CircuitBreaker; beforeEach(() => { breaker = new CircuitBreaker({ failureThreshold: 3, successThreshold: 1, timeout: 1000 }); }); describe('canExecute', () => { it('should allow execution when closed', () => { expect(breaker.canExecute()).toBe(true); }); it('should prevent execution when open', () => { breaker.recordFailure(); breaker.recordFailure(); breaker.recordFailure(); expect(breaker.canExecute()).toBe(false); }); }); describe('recordSuccess', () => { it('should reset failure count on success', () => { breaker.recordFailure(); breaker.recordFailure(); breaker.recordSuccess(); // Should be closed again expect(breaker.canExecute()).toBe(true); }); }); describe('recordFailure', () => { it('should increment failure count', () => { breaker.recordFailure(); expect(breaker.canExecute()).toBe(true); breaker.recordFailure(); expect(breaker.canExecute()).toBe(true); breaker.recordFailure(); expect(breaker.canExecute()).toBe(false); }); }); }); describe('CircuitBreakerRegistry', () => { it('should return singleton instance', () => { const registry1 = CircuitBreakerRegistry.getInstance(); const registry2 = CircuitBreakerRegistry.getInstance(); expect(registry1).toBe(registry2); }); it('should return same breaker for same path', () => { const registry = CircuitBreakerRegistry.getInstance(); const breaker1 = registry.getBreaker('/api/test'); const breaker2 = registry.getBreaker('/api/test'); expect(breaker1).toBe(breaker2); }); it('should return different breakers for different paths', () => { const registry = CircuitBreakerRegistry.getInstance(); const breaker1 = registry.getBreaker('/api/test1'); const breaker2 = registry.getBreaker('/api/test2'); expect(breaker1).not.toBe(breaker2); }); });