Skip to main content
Glama
circuit-breaker.test.ts9.47 kB
/** * Tests for Circuit Breaker implementation */ import { CircuitBreaker, CircuitBreakerManager, CircuitState } from '@/teamcity/circuit-breaker'; // Mock the logger jest.mock('@/utils/logger', () => ({ info: jest.fn(), warn: jest.fn(), })); describe('CircuitBreaker', () => { let breaker: CircuitBreaker; beforeEach(() => { jest.clearAllMocks(); breaker = new CircuitBreaker('test-endpoint', { failureThreshold: 3, resetTimeout: 100, // 100ms for testing successThreshold: 2, }); }); describe('CLOSED state', () => { it('should start in CLOSED state', () => { expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should execute function successfully in CLOSED state', async () => { const result = await breaker.execute(async () => 'success'); expect(result).toBe('success'); expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should count failures and stay CLOSED below threshold', async () => { const failingFn = async () => { throw new Error('failure'); }; // First failure await expect(breaker.execute(failingFn)).rejects.toThrow('failure'); expect(breaker.getState()).toBe(CircuitState.CLOSED); // Second failure await expect(breaker.execute(failingFn)).rejects.toThrow('failure'); expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should transition to OPEN after reaching failure threshold', async () => { const failingFn = async () => { throw new Error('failure'); }; // Fail 3 times (threshold) /* eslint-disable no-await-in-loop */ for (let i = 0; i < 3; i++) { await expect(breaker.execute(failingFn)).rejects.toThrow('failure'); } /* eslint-enable no-await-in-loop */ expect(breaker.getState()).toBe(CircuitState.OPEN); }); it('should reset failure count on success', async () => { const failingFn = async () => { throw new Error('failure'); }; const successFn = async () => 'success'; // Two failures await expect(breaker.execute(failingFn)).rejects.toThrow(); await expect(breaker.execute(failingFn)).rejects.toThrow(); // Success should reset count await breaker.execute(successFn); const stats = breaker.getStats(); expect(stats.failureCount).toBe(0); expect(breaker.getState()).toBe(CircuitState.CLOSED); }); }); describe('OPEN state', () => { beforeEach(async () => { // Open the circuit const failingFn = async () => { throw new Error('failure'); }; /* eslint-disable no-await-in-loop */ for (let i = 0; i < 3; i++) { await expect(breaker.execute(failingFn)).rejects.toThrow(); } /* eslint-enable no-await-in-loop */ }); it('should reject immediately in OPEN state', async () => { const fn = jest.fn(async () => 'success'); await expect(breaker.execute(fn)).rejects.toThrow('Circuit breaker is OPEN'); expect(fn).not.toHaveBeenCalled(); }); it('should transition to HALF_OPEN after reset timeout', async () => { expect(breaker.getState()).toBe(CircuitState.OPEN); // Wait for reset timeout await new Promise((resolve) => setTimeout(resolve, 150)); // Next execution should transition to HALF_OPEN const fn = async () => 'success'; await breaker.execute(fn); // After one success, still HALF_OPEN expect(breaker.getState()).toBe(CircuitState.HALF_OPEN); }); }); describe('HALF_OPEN state', () => { beforeEach(async () => { // Open the circuit const failingFn = async () => { throw new Error('failure'); }; /* eslint-disable no-await-in-loop */ for (let i = 0; i < 3; i++) { await expect(breaker.execute(failingFn)).rejects.toThrow(); } /* eslint-enable no-await-in-loop */ // Wait for reset timeout await new Promise((resolve) => setTimeout(resolve, 150)); }); it('should transition to CLOSED after success threshold', async () => { const successFn = async () => 'success'; // First success - transitions to HALF_OPEN await breaker.execute(successFn); expect(breaker.getState()).toBe(CircuitState.HALF_OPEN); // Second success - should transition to CLOSED await breaker.execute(successFn); expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should transition back to OPEN on failure in HALF_OPEN', async () => { const successFn = async () => 'success'; const failingFn = async () => { throw new Error('failure'); }; // First execution transitions to HALF_OPEN await breaker.execute(successFn); expect(breaker.getState()).toBe(CircuitState.HALF_OPEN); // Failure should transition back to OPEN await expect(breaker.execute(failingFn)).rejects.toThrow('failure'); expect(breaker.getState()).toBe(CircuitState.OPEN); }); }); describe('getStats', () => { it('should return circuit statistics', async () => { const stats = breaker.getStats(); expect(stats).toHaveProperty('state', CircuitState.CLOSED); expect(stats).toHaveProperty('failureCount', 0); expect(stats).toHaveProperty('successCount', 0); expect(stats).toHaveProperty('lastFailureTime', undefined); }); it('should track failure statistics', async () => { const failingFn = async () => { throw new Error('failure'); }; await expect(breaker.execute(failingFn)).rejects.toThrow(); const stats = breaker.getStats(); expect(stats.failureCount).toBe(1); expect(stats.lastFailureTime).toBeDefined(); }); }); describe('reset', () => { it('should reset circuit to CLOSED state', async () => { // Open the circuit const failingFn = async () => { throw new Error('failure'); }; /* eslint-disable no-await-in-loop */ for (let i = 0; i < 3; i++) { await expect(breaker.execute(failingFn)).rejects.toThrow(); } /* eslint-enable no-await-in-loop */ expect(breaker.getState()).toBe(CircuitState.OPEN); // Reset breaker.reset(); expect(breaker.getState()).toBe(CircuitState.CLOSED); const stats = breaker.getStats(); expect(stats.failureCount).toBe(0); expect(stats.successCount).toBe(0); expect(stats.lastFailureTime).toBeUndefined(); }); }); }); describe('CircuitBreakerManager', () => { let manager: CircuitBreakerManager; beforeEach(() => { manager = new CircuitBreakerManager({ failureThreshold: 2, resetTimeout: 100, }); }); it('should create breakers for different endpoints', () => { const breaker1 = manager.getBreaker('/api/builds'); const breaker2 = manager.getBreaker('/api/projects'); expect(breaker1).toBeDefined(); expect(breaker2).toBeDefined(); expect(breaker1).not.toBe(breaker2); }); it('should reuse breaker for same endpoint', () => { const breaker1 = manager.getBreaker('/api/builds'); const breaker2 = manager.getBreaker('/api/builds'); expect(breaker1).toBe(breaker2); }); it('should execute with circuit breaker', async () => { const result = await manager.execute('/api/builds', async () => 'success'); expect(result).toBe('success'); }); it('should track stats for all breakers', async () => { // Create some breakers with activity await manager.execute('/api/builds', async () => 'success'); await manager .execute('/api/projects', async () => { throw new Error('failure'); }) .catch(() => {}); const stats = manager.getAllStats(); expect(stats).toHaveProperty('/api/builds'); expect(stats).toHaveProperty('/api/projects'); expect(stats['/api/builds']?.state).toBe(CircuitState.CLOSED); expect(stats['/api/projects']?.failureCount).toBe(1); }); it('should reset all breakers', async () => { // Open some circuits const failingFn = async () => { throw new Error('failure'); }; /* eslint-disable no-await-in-loop */ for (let i = 0; i < 2; i++) { await manager.execute('/api/builds', failingFn).catch(() => {}); await manager.execute('/api/projects', failingFn).catch(() => {}); } /* eslint-enable no-await-in-loop */ const statsBefore = manager.getAllStats(); expect(statsBefore['/api/builds']?.state).toBe(CircuitState.OPEN); expect(statsBefore['/api/projects']?.state).toBe(CircuitState.OPEN); // Reset all manager.resetAll(); const statsAfter = manager.getAllStats(); expect(statsAfter['/api/builds']?.state).toBe(CircuitState.CLOSED); expect(statsAfter['/api/projects']?.state).toBe(CircuitState.CLOSED); }); it('should reset specific breaker', async () => { // Open a circuit const failingFn = async () => { throw new Error('failure'); }; /* eslint-disable no-await-in-loop */ for (let i = 0; i < 2; i++) { await manager.execute('/api/builds', failingFn).catch(() => {}); } /* eslint-enable no-await-in-loop */ expect(manager.getBreaker('/api/builds').getState()).toBe(CircuitState.OPEN); // Reset specific manager.reset('/api/builds'); expect(manager.getBreaker('/api/builds').getState()).toBe(CircuitState.CLOSED); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server