Skip to main content
Glama
bradcstevens

Copilot Studio Agent Direct Line MCP Server

by bradcstevens
retry.test.ts9.72 kB
/** * Tests for retry utilities */ import { retryWithBackoff, retryWithStrategy, retryOAuthOperation, retryWithCircuitBreaker, retryWithDeadline, ErrorType, classifyError, isRetryableError, } from '../../../src/utils/retry.js'; import { CircuitState } from '../../../src/utils/circuit-breaker.js'; import { AxiosError } from 'axios'; // Helper to create mock AxiosError function createAxiosError(status: number, message: string): AxiosError { const error = new Error(message) as AxiosError; error.isAxiosError = true; error.response = { status, statusText: message, data: {}, headers: {}, config: {headers: {} as any} as any, }; error.config = {headers: {} as any} as any; error.name = 'AxiosError'; return error; } describe('Error Classification', () => { it('should classify network errors', () => { const error = createAxiosError(500, 'Network error'); error.response = undefined; // No response = network error const type = classifyError(error); expect(type).toBe(ErrorType.NETWORK); }); it('should classify authentication errors', () => { const error = createAxiosError(401, 'Unauthorized'); const type = classifyError(error); expect(type).toBe(ErrorType.AUTHENTICATION); }); it('should classify rate limit errors', () => { const error = createAxiosError(429, 'Too Many Requests'); const type = classifyError(error); expect(type).toBe(ErrorType.RATE_LIMIT); }); it('should classify server errors', () => { const error = createAxiosError(500, 'Internal Server Error'); const type = classifyError(error); expect(type).toBe(ErrorType.SERVER); }); it('should classify client errors', () => { const error = createAxiosError(400, 'Bad Request'); const type = classifyError(error); expect(type).toBe(ErrorType.CLIENT); }); }); describe('isRetryableError', () => { it('should mark network errors as retryable', () => { const error = createAxiosError(500, 'Network error'); error.response = undefined; // No response = network error expect(isRetryableError(error)).toBe(true); }); it('should mark rate limit errors as retryable', () => { const error = createAxiosError(429, 'Too Many Requests'); expect(isRetryableError(error)).toBe(true); }); it('should mark server errors as retryable', () => { const error = createAxiosError(500, 'Internal Server Error'); expect(isRetryableError(error)).toBe(true); }); it('should not mark auth errors as retryable', () => { const error = createAxiosError(401, 'Unauthorized'); expect(isRetryableError(error)).toBe(false); }); it('should not mark client errors as retryable', () => { const error = createAxiosError(400, 'Bad Request'); expect(isRetryableError(error)).toBe(false); }); }); describe('retryWithBackoff', () => { it('should succeed on first attempt', async () => { const fn = jest.fn().mockResolvedValue('success'); const result = await retryWithBackoff(fn); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(1); }); it('should retry on retryable errors', async () => { let attempts = 0; const fn = jest.fn().mockImplementation(async () => { attempts++; if (attempts < 3) { const error = createAxiosError(500, 'Server Error'); error.response = undefined; // Make it a network error throw error; } return 'success'; }); const result = await retryWithBackoff(fn, { maxRetries: 5, baseDelay: 10, maxDelay: 50 }); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(3); }, 10000); it('should not retry on non-retryable errors', async () => { const fn = jest.fn().mockRejectedValue(createAxiosError(401, 'Unauthorized')); await expect( retryWithBackoff(fn, { maxRetries: 3 }) ).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(1); }); it('should exhaust retries and throw last error', async () => { const fn = jest.fn().mockImplementation(async () => { const error = createAxiosError(500, 'Server Error'); error.response = undefined; // Make it retryable throw error; }); await expect( retryWithBackoff(fn, { maxRetries: 2, baseDelay: 10 }) ).rejects.toThrow('Server Error'); expect(fn).toHaveBeenCalledTimes(3); // Initial + 2 retries }, 10000); it('should call onRetry callback', async () => { let attempts = 0; const fn = jest.fn().mockImplementation(async () => { attempts++; if (attempts < 2) { const error = createAxiosError(503, 'Service Unavailable'); error.response = undefined; // Make it retryable throw error; } return 'success'; }); const onRetry = jest.fn(); await retryWithBackoff(fn, { maxRetries: 3, baseDelay: 10, onRetry }); expect(onRetry).toHaveBeenCalledTimes(1); expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error)); }, 10000); }); describe('retryWithStrategy', () => { it('should retry with exponential backoff and jitter', async () => { let attempts = 0; const fn = jest.fn().mockImplementation(async () => { attempts++; if (attempts < 3) { const error = new Error('Network error'); error.name = 'NetworkError'; throw error; } return 'success'; }); const result = await retryWithStrategy(fn, { strategy: { maxRetries: 5, initialDelay: 100, maxDelay: 1000, backoffMultiplier: 2, }, }); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(3); }); it('should respect custom shouldRetry logic', async () => { const fn = jest.fn().mockRejectedValue(new Error('Custom error')); await expect( retryWithStrategy(fn, { shouldRetry: () => false, }) ).rejects.toThrow('Custom error'); expect(fn).toHaveBeenCalledTimes(1); }); it('should call onRetry with delay information', async () => { let attempts = 0; const fn = jest.fn().mockImplementation(async () => { attempts++; if (attempts < 2) { const error = new Error('Test error') as Error & { status?: number }; error.status = 500; throw error; } return 'success'; }); const onRetry = jest.fn(); await retryWithStrategy(fn, { strategy: { maxRetries: 3 }, onRetry, }); expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error), expect.any(Number)); }); }); describe('retryOAuthOperation', () => { it('should not retry authentication errors', async () => { const fn = jest.fn().mockImplementation(async () => { const error = new Error('Auth failed'); error.name = 'AuthenticationError'; throw error; }); await expect(retryOAuthOperation(fn)).rejects.toThrow('Auth failed'); expect(fn).toHaveBeenCalledTimes(1); }); it('should not retry invalid_grant errors', async () => { const fn = jest.fn().mockRejectedValue(new Error('invalid_grant: Token expired')); await expect(retryOAuthOperation(fn)).rejects.toThrow(); expect(fn).toHaveBeenCalledTimes(1); }); it('should retry server errors', async () => { let attempts = 0; const fn = jest.fn().mockImplementation(async () => { attempts++; if (attempts < 2) { const error = new Error('Server error') as Error & { status?: number }; error.status = 500; throw error; } return 'success'; }); const result = await retryOAuthOperation(fn); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(2); }); }); describe('retryWithCircuitBreaker', () => { it('should fail if circuit breaker is open', async () => { const fn = jest.fn().mockResolvedValue('success'); const circuitBreakerCheck = jest.fn().mockReturnValue(false); await expect( retryWithCircuitBreaker(fn, {}, circuitBreakerCheck) ).rejects.toThrow('Circuit breaker is open'); expect(fn).not.toHaveBeenCalled(); expect(circuitBreakerCheck).toHaveBeenCalled(); }); it('should execute if circuit breaker is closed', async () => { const fn = jest.fn().mockResolvedValue('success'); const circuitBreakerCheck = jest.fn().mockReturnValue(true); const result = await retryWithCircuitBreaker(fn, {}, circuitBreakerCheck); expect(result).toBe('success'); expect(fn).toHaveBeenCalled(); }); }); describe('retryWithDeadline', () => { it('should stop retrying after deadline', async () => { const fn = jest.fn().mockImplementation(async () => { await new Promise(resolve => setTimeout(resolve, 150)); const error = new Error('Test error') as Error & { status?: number }; error.status = 500; throw error; }); await expect( retryWithDeadline(fn, 400, { strategy: { maxRetries: 10, initialDelay: 100 }, }) ).rejects.toThrow('Test error'); // Should have tried 2-3 times before deadline expect(fn).toHaveBeenCalled(); expect(fn.mock.calls.length).toBeLessThanOrEqual(4); }, 15000); it('should succeed if completed before deadline', async () => { let attempts = 0; const fn = jest.fn().mockImplementation(async () => { attempts++; if (attempts < 3) { const error = new Error('Test error') as Error & { status?: number }; error.status = 500; throw error; } return 'success'; }); const result = await retryWithDeadline(fn, 5000, { strategy: { maxRetries: 5, initialDelay: 10 }, }); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(3); }, 10000); });

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