import { describe, it, expect } from 'vitest'; import { CircuitBreaker, CircuitBreakerError, CircuitState } from '../../src/utils/circuit-breaker'; function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } describe('CircuitBreaker', () => { it('starts in CLOSED state', () => { const cb = new CircuitBreaker({ serviceName: 'test' }); expect(cb.getState()).toBe(CircuitState.CLOSED); }); it('passes through successful executions', async () => { const cb = new CircuitBreaker({ serviceName: 'test' }); const result = await cb.execute(async () => 'ok'); expect(result).toBe('ok'); expect(cb.getState()).toBe(CircuitState.CLOSED); }); it('opens after failure threshold is exceeded', async () => { const cb = new CircuitBreaker({ serviceName: 'test', failureThreshold: 3, resetTimeoutMs: 100, }); for (let i = 0; i < 3; i++) { await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow('fail'); } expect(cb.getState()).toBe(CircuitState.OPEN); }); it('rejects requests while OPEN', async () => { const cb = new CircuitBreaker({ serviceName: 'test', failureThreshold: 2, resetTimeoutMs: 5000, }); // Trip the breaker for (let i = 0; i < 2; i++) { await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow(); } expect(cb.getState()).toBe(CircuitState.OPEN); // Should throw CircuitBreakerError await expect(cb.execute(async () => 'ok')).rejects.toThrow(CircuitBreakerError); }); it('transitions to HALF_OPEN after reset timeout', async () => { const cb = new CircuitBreaker({ serviceName: 'test', failureThreshold: 2, resetTimeoutMs: 100, }); for (let i = 0; i < 2; i++) { await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow(); } expect(cb.getState()).toBe(CircuitState.OPEN); await sleep(150); // Next execute call checks the timeout and transitions to HALF_OPEN const result = await cb.execute(async () => 'recovered'); expect(result).toBe('recovered'); // Successful test in HALF_OPEN closes the circuit expect(cb.getState()).toBe(CircuitState.CLOSED); }); it('closes after successful test in HALF_OPEN', async () => { const cb = new CircuitBreaker({ serviceName: 'test', failureThreshold: 2, resetTimeoutMs: 100, }); // Open the circuit for (let i = 0; i < 2; i++) { await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow(); } expect(cb.getState()).toBe(CircuitState.OPEN); // Wait for reset timeout await sleep(150); // Successful execution transitions HALF_OPEN -> CLOSED await cb.execute(async () => 'success'); expect(cb.getState()).toBe(CircuitState.CLOSED); // Verify circuit is fully operational again const result = await cb.execute(async () => 'working'); expect(result).toBe('working'); }); it('re-opens if HALF_OPEN test fails', async () => { const cb = new CircuitBreaker({ serviceName: 'test', failureThreshold: 2, resetTimeoutMs: 100, }); // Open the circuit for (let i = 0; i < 2; i++) { await expect(cb.execute(async () => { throw new Error('fail'); })).rejects.toThrow(); } await sleep(150); // Fail during HALF_OPEN await expect(cb.execute(async () => { throw new Error('still broken'); })).rejects.toThrow(); expect(cb.getState()).toBe(CircuitState.OPEN); }); });