circuit-breaker.test.ts•8.47 kB
/**
* Circuit Breaker Tests
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { CircuitBreaker, CircuitBreakerState, CircuitBreakerFactory } from './circuit-breaker';
describe('CircuitBreaker', () => {
let circuitBreaker: CircuitBreaker;
beforeEach(() => {
circuitBreaker = new CircuitBreaker({
name: 'test-breaker',
failureThreshold: 3,
recoveryTimeout: 1000,
successThreshold: 2,
timeout: 5000,
monitoringWindow: 10000,
maxRetries: 2,
exponentialBackoff: true,
jitterEnabled: false
});
});
afterEach(async () => {
await circuitBreaker.cleanup();
});
describe('Basic Operation', () => {
it('should execute successful operations normally', async () => {
const operation = vi.fn().mockResolvedValue('success');
const result = await circuitBreaker.execute(operation);
expect(result).toBe('success');
expect(operation).toHaveBeenCalledOnce();
});
it('should handle operation failures', async () => {
const operation = vi.fn().mockRejectedValue(new Error('Operation failed'));
await expect(circuitBreaker.execute(operation)).rejects.toThrow('Operation failed');
});
it('should transition to OPEN state after threshold failures', async () => {
const operation = vi.fn().mockRejectedValue(new Error('Failure'));
// Fail 3 times to trigger the threshold
for (let i = 0; i < 3; i++) {
try {
await circuitBreaker.execute(operation);
} catch (error) {
// Expected failures
}
}
const metrics = circuitBreaker.getMetrics();
expect(metrics.state).toBe(CircuitBreakerState.OPEN);
});
it('should reject requests when circuit is OPEN', async () => {
const operation = vi.fn().mockRejectedValue(new Error('Failure'));
// Force circuit to open
circuitBreaker.forceState(CircuitBreakerState.OPEN);
await expect(circuitBreaker.execute(operation)).rejects.toThrow('Circuit breaker');
expect(operation).not.toHaveBeenCalled();
});
});
describe('State Transitions', () => {
it('should transition to HALF_OPEN after recovery timeout', async () => {
const operation = vi.fn()
.mockRejectedValueOnce(new Error('Failure'))
.mockRejectedValueOnce(new Error('Failure'))
.mockRejectedValueOnce(new Error('Failure'))
.mockResolvedValue('success');
// Force failures to open circuit
for (let i = 0; i < 3; i++) {
try {
await circuitBreaker.execute(operation);
} catch (error) {
// Expected
}
}
expect(circuitBreaker.getMetrics().state).toBe(CircuitBreakerState.OPEN);
// Wait for recovery timeout
await new Promise(resolve => setTimeout(resolve, 1100));
// Next call should transition to HALF_OPEN and succeed
const result = await circuitBreaker.execute(operation);
expect(result).toBe('success');
});
it('should transition to CLOSED after successful recovery', async () => {
const operation = vi.fn().mockResolvedValue('success');
// Start in HALF_OPEN state
circuitBreaker.forceState(CircuitBreakerState.HALF_OPEN);
// Execute successful operations to meet success threshold
await circuitBreaker.execute(operation);
await circuitBreaker.execute(operation);
const metrics = circuitBreaker.getMetrics();
expect(metrics.state).toBe(CircuitBreakerState.CLOSED);
});
});
describe('Retry Logic', () => {
it('should retry failed operations up to maxRetries', async () => {
const operation = vi.fn()
.mockRejectedValueOnce(new Error('Temporary failure'))
.mockRejectedValueOnce(new Error('Temporary failure'))
.mockResolvedValue('success');
const result = await circuitBreaker.execute(operation);
expect(result).toBe('success');
expect(operation).toHaveBeenCalledTimes(3);
});
it('should respect timeout limits', async () => {
const slowOperation = () => new Promise(resolve => setTimeout(resolve, 6000));
await expect(circuitBreaker.execute(slowOperation)).rejects.toThrow('timeout');
});
});
describe('Metrics', () => {
it('should track failure and success counts', async () => {
const successOp = vi.fn().mockResolvedValue('success');
const failOp = vi.fn().mockRejectedValue(new Error('failure'));
await circuitBreaker.execute(successOp);
try {
await circuitBreaker.execute(failOp);
} catch (error) {
// Expected
}
const metrics = circuitBreaker.getMetrics();
expect(metrics.successCount).toBe(1);
expect(metrics.failureCount).toBe(1);
expect(metrics.totalRequests).toBe(2);
});
it('should calculate response time metrics', async () => {
const operation = vi.fn().mockResolvedValue('success');
await circuitBreaker.execute(operation);
const metrics = circuitBreaker.getMetrics();
expect(metrics.responseTime.average).toBeGreaterThan(0);
});
});
describe('Circuit Breaker Factory', () => {
afterEach(async () => {
await CircuitBreakerFactory.cleanup();
});
it('should create LLM circuit breaker with correct config', () => {
const breaker = CircuitBreakerFactory.createLLMCircuitBreaker();
const metrics = breaker.getMetrics();
expect(metrics.state).toBe(CircuitBreakerState.CLOSED);
});
it('should return same instance for same name', () => {
const breaker1 = CircuitBreakerFactory.createLLMCircuitBreaker('test');
const breaker2 = CircuitBreakerFactory.createLLMCircuitBreaker('test');
expect(breaker1).toBe(breaker2);
});
it('should create trajectory circuit breaker with appropriate settings', () => {
const breaker = CircuitBreakerFactory.createTrajectoryCircuitBreaker();
const metrics = breaker.getMetrics();
expect(metrics.state).toBe(CircuitBreakerState.CLOSED);
});
});
describe('Events', () => {
it('should emit success events', async () => {
const successHandler = vi.fn();
circuitBreaker.on('success', successHandler);
const operation = vi.fn().mockResolvedValue('success');
await circuitBreaker.execute(operation);
expect(successHandler).toHaveBeenCalledWith(expect.objectContaining({
name: 'test-breaker',
state: CircuitBreakerState.CLOSED
}));
});
it('should emit failure events', async () => {
const failureHandler = vi.fn();
circuitBreaker.on('failure', failureHandler);
const operation = vi.fn().mockRejectedValue(new Error('Test error'));
try {
await circuitBreaker.execute(operation);
} catch (error) {
// Expected
}
expect(failureHandler).toHaveBeenCalledWith(expect.objectContaining({
name: 'test-breaker',
error: expect.any(Error)
}));
});
it('should emit state change events', async () => {
const stateChangeHandler = vi.fn();
circuitBreaker.on('stateChange', stateChangeHandler);
const operation = vi.fn().mockRejectedValue(new Error('Failure'));
// Trigger state change to OPEN
for (let i = 0; i < 3; i++) {
try {
await circuitBreaker.execute(operation);
} catch (error) {
// Expected
}
}
expect(stateChangeHandler).toHaveBeenCalledWith(expect.objectContaining({
name: 'test-breaker',
from: 'CLOSED',
to: 'OPEN'
}));
});
});
describe('Edge Cases', () => {
it('should handle operations that throw non-Error objects', async () => {
const operation = vi.fn().mockRejectedValue('String error');
await expect(circuitBreaker.execute(operation)).rejects.toBe('String error');
});
it('should reset properly', () => {
// Force some failures
circuitBreaker.forceState(CircuitBreakerState.OPEN);
circuitBreaker.reset();
const metrics = circuitBreaker.getMetrics();
expect(metrics.state).toBe(CircuitBreakerState.CLOSED);
expect(metrics.failureCount).toBe(0);
expect(metrics.successCount).toBe(0);
});
});
});