Skip to main content
Glama
bradcstevens

Copilot Studio Agent Direct Line MCP Server

by bradcstevens
circuit-breaker.test.ts9.25 kB
/** * Tests for Circuit Breaker implementation */ import { CircuitBreaker, CircuitState, FailureType, CircuitBreakerError } from '../../../src/utils/circuit-breaker.js'; // Helper to wait for a specified time const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); describe('CircuitBreaker', () => { describe('Basic functionality', () => { it('should start in CLOSED state', () => { const breaker = new CircuitBreaker(); expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should execute function successfully when CLOSED', async () => { const breaker = new CircuitBreaker(); const result = await breaker.execute(async () => 'success'); expect(result).toBe('success'); }); it('should propagate errors from executed function', async () => { const breaker = new CircuitBreaker(); await expect( breaker.execute(async () => { throw new Error('Test error'); }) ).rejects.toThrow('Test error'); }); }); describe('State transitions', () => { it('should open circuit after reaching failure threshold', async () => { const breaker = new CircuitBreaker({ failureThreshold: 3, failureWindow: 10000, recoveryTimeout: 5000, successThreshold: 2, }); // Trigger 3 failures for (let i = 0; i < 3; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } expect(breaker.getState()).toBe(CircuitState.OPEN); }); it('should fail fast when OPEN', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 5000, successThreshold: 2, }); // Open the circuit for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } // Should fail fast await expect( breaker.execute(async () => 'success') ).rejects.toThrow(CircuitBreakerError); const metrics = breaker.getMetrics(); expect(metrics.rejections).toBeGreaterThan(0); }); it('should transition to HALF_OPEN after recovery timeout', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 100, // Short timeout for testing successThreshold: 2, }); // Open the circuit for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } expect(breaker.getState()).toBe(CircuitState.OPEN); // Wait for recovery timeout await wait(150); // Next call should trigger HALF_OPEN check try { await breaker.execute(async () => 'success'); } catch (error) { // May fail if state check happens before execution } // Circuit should be HALF_OPEN or CLOSED const state = breaker.getState(); expect([CircuitState.HALF_OPEN, CircuitState.CLOSED]).toContain(state); }); it('should close circuit after successful recoveries in HALF_OPEN', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 100, successThreshold: 2, }); // Open the circuit for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } // Wait for recovery await wait(150); // Execute successful operations await breaker.execute(async () => 'success1'); await breaker.execute(async () => 'success2'); expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should reopen circuit on failure in HALF_OPEN', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 100, successThreshold: 2, }); // Open the circuit for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } // Wait for recovery await wait(150); // Fail during HALF_OPEN try { await breaker.execute(async () => { throw new Error('Still failing'); }); } catch (error) { // Expected } expect(breaker.getState()).toBe(CircuitState.OPEN); }); }); describe('Failure window', () => { it('should only count failures within window', async () => { const breaker = new CircuitBreaker({ failureThreshold: 3, failureWindow: 200, // 200ms window recoveryTimeout: 5000, successThreshold: 2, }); // Trigger 2 failures for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } // Wait for window to expire await wait(250); // Trigger 2 more failures (old ones should be expired) for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } // Should still be CLOSED (only 2 failures in window) expect(breaker.getState()).toBe(CircuitState.CLOSED); }); }); describe('Failure type classification', () => { it('should exclude configured failure types from count', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 5000, successThreshold: 2, excludedFailureTypes: [FailureType.AUTH_SERVICE], }); // Create auth errors (should be excluded) for (let i = 0; i < 3; i++) { try { await breaker.execute(async () => { const error = new Error('Auth failed') as Error & { name: string; status?: number }; error.name = 'AuthenticationError'; error.status = 401; throw error; }); } catch (error) { // Expected } } // Circuit should remain CLOSED (auth errors excluded) expect(breaker.getState()).toBe(CircuitState.CLOSED); }); it('should count non-excluded failure types', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 5000, successThreshold: 2, excludedFailureTypes: [FailureType.AUTH_SERVICE], }); // Create network errors (should count) for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { const error = new Error('Network failed'); error.name = 'NetworkError'; throw error; }); } catch (error) { // Expected } } // Circuit should be OPEN (network errors counted) expect(breaker.getState()).toBe(CircuitState.OPEN); }); }); describe('Metrics', () => { it('should track metrics correctly', async () => { const breaker = new CircuitBreaker({ failureThreshold: 3, failureWindow: 10000, recoveryTimeout: 5000, successThreshold: 2, }); // Execute some operations await breaker.execute(async () => 'success1'); await breaker.execute(async () => 'success2'); try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } const metrics = breaker.getMetrics(); expect(metrics.state).toBe(CircuitState.CLOSED); expect(metrics.failures).toBe(1); expect(metrics.lastFailureTime).toBeTruthy(); }); }); describe('Reset', () => { it('should reset circuit to CLOSED state', async () => { const breaker = new CircuitBreaker({ failureThreshold: 2, failureWindow: 10000, recoveryTimeout: 5000, successThreshold: 2, }); // Open the circuit for (let i = 0; i < 2; i++) { try { await breaker.execute(async () => { throw new Error('Failure'); }); } catch (error) { // Expected } } expect(breaker.getState()).toBe(CircuitState.OPEN); // Reset breaker.reset(); expect(breaker.getState()).toBe(CircuitState.CLOSED); const metrics = breaker.getMetrics(); expect(metrics.failures).toBe(0); expect(metrics.successes).toBe(0); expect(metrics.rejections).toBe(0); }); }); });

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/bradcstevens/copilot-studio-agent-direct-line-mcp'

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