/* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ import { handleOpenAIError, isRetryableError, getRetryDelay } from '../src/utils/error-handling'; import { AIProviderError } from '@bernierllc/ai-provider-core'; import OpenAI from 'openai'; describe('Error Handling', () => { describe('handleOpenAIError()', () => { it('should convert OpenAI APIError to AIProviderError', () => { const apiError = new OpenAI.APIError( 400, { error: { message: 'Invalid request', type: 'invalid_request_error', param: 'model', code: 'invalid_model' } }, 'Invalid request', {} ); const result = handleOpenAIError(apiError); expect(result).toBeInstanceOf(AIProviderError); expect(result.message).toContain('OpenAI API Error'); expect(result.message).toContain('Invalid request'); expect(result.code).toBe('INVALID_REQUEST'); expect(result.provider).toBe('openai'); }); it('should map 401 status to AUTHENTICATION_ERROR', () => { const apiError = new OpenAI.APIError( 401, { error: { message: 'Invalid API key' } }, 'Invalid API key', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('AUTHENTICATION_ERROR'); }); it('should map 403 status to PERMISSION_DENIED', () => { const apiError = new OpenAI.APIError( 403, { error: { message: 'Permission denied' } }, 'Permission denied', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('PERMISSION_DENIED'); }); it('should map 404 status to NOT_FOUND', () => { const apiError = new OpenAI.APIError( 404, { error: { message: 'Model not found' } }, 'Model not found', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('NOT_FOUND'); }); it('should map 429 status to RATE_LIMIT_ERROR', () => { const apiError = new OpenAI.APIError( 429, { error: { message: 'Rate limit exceeded' } }, 'Rate limit exceeded', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('RATE_LIMIT_ERROR'); }); it('should map 500 status to SERVER_ERROR', () => { const apiError = new OpenAI.APIError( 500, { error: { message: 'Internal server error' } }, 'Internal server error', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('SERVER_ERROR'); }); it('should map 502 status to SERVER_ERROR', () => { const apiError = new OpenAI.APIError( 502, { error: { message: 'Bad gateway' } }, 'Bad gateway', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('SERVER_ERROR'); }); it('should map 503 status to SERVER_ERROR', () => { const apiError = new OpenAI.APIError( 503, { error: { message: 'Service unavailable' } }, 'Service unavailable', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('SERVER_ERROR'); }); it('should map 504 status to TIMEOUT_ERROR', () => { const apiError = new OpenAI.APIError( 504, { error: { message: 'Gateway timeout' } }, 'Gateway timeout', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('TIMEOUT_ERROR'); }); it('should map unknown status codes to UNKNOWN_ERROR', () => { const apiError = new OpenAI.APIError( 418, { error: { message: "I'm a teapot" } }, "I'm a teapot", {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('UNKNOWN_ERROR'); }); it('should handle APIError with undefined status', () => { const apiError = new OpenAI.APIError( undefined as any, { error: { message: 'No status' } }, 'No status', {} ); const result = handleOpenAIError(apiError); expect(result.code).toBe('UNKNOWN_ERROR'); }); it('should handle regular Error objects', () => { const error = new Error('Something went wrong'); const result = handleOpenAIError(error); expect(result).toBeInstanceOf(AIProviderError); expect(result.message).toBe('Something went wrong'); expect(result.code).toBe('UNKNOWN_ERROR'); }); it('should handle unknown error types', () => { const result = handleOpenAIError('string error'); expect(result).toBeInstanceOf(AIProviderError); expect(result.message).toBe('Unknown error occurred'); expect(result.code).toBe('UNKNOWN_ERROR'); }); it('should include error details in AIProviderError', () => { const apiError = new OpenAI.APIError( 400, { error: { message: 'Invalid request', type: 'invalid_request_error', param: 'model', code: 'invalid_model' } }, 'Invalid request', {} ); const result = handleOpenAIError(apiError); expect(result.details).toBeDefined(); expect(result.details?.status).toBe(400); // OpenAI.APIError type and code properties may not be available on all versions // So we just verify the details object exists }); }); describe('isRetryableError()', () => { it('should identify RATE_LIMIT_ERROR as retryable', () => { const error = new AIProviderError('Rate limit', 'RATE_LIMIT_ERROR', 'openai'); expect(isRetryableError(error)).toBe(true); }); it('should identify SERVER_ERROR as retryable', () => { const error = new AIProviderError('Server error', 'SERVER_ERROR', 'openai'); expect(isRetryableError(error)).toBe(true); }); it('should identify TIMEOUT_ERROR as retryable', () => { const error = new AIProviderError('Timeout', 'TIMEOUT_ERROR', 'openai'); expect(isRetryableError(error)).toBe(true); }); it('should identify NETWORK_ERROR as retryable', () => { const error = new AIProviderError('Network error', 'NETWORK_ERROR', 'openai'); expect(isRetryableError(error)).toBe(true); }); it('should identify AUTHENTICATION_ERROR as not retryable', () => { const error = new AIProviderError('Auth error', 'AUTHENTICATION_ERROR', 'openai'); expect(isRetryableError(error)).toBe(false); }); it('should identify INVALID_REQUEST as not retryable', () => { const error = new AIProviderError('Invalid', 'INVALID_REQUEST', 'openai'); expect(isRetryableError(error)).toBe(false); }); it('should identify PERMISSION_DENIED as not retryable', () => { const error = new AIProviderError('Denied', 'PERMISSION_DENIED', 'openai'); expect(isRetryableError(error)).toBe(false); }); }); describe('getRetryDelay()', () => { it('should return exponential backoff for rate limit errors', () => { const error = new AIProviderError('Rate limit', 'RATE_LIMIT_ERROR', 'openai'); const delay1 = getRetryDelay(error, 1); const delay2 = getRetryDelay(error, 2); const delay3 = getRetryDelay(error, 3); expect(delay1).toBe(2000); // 2^1 * 1000 expect(delay2).toBe(4000); // 2^2 * 1000 expect(delay3).toBe(8000); // 2^3 * 1000 }); it('should return exponential backoff for timeout errors', () => { const error = new AIProviderError('Timeout', 'TIMEOUT_ERROR', 'openai'); const delay1 = getRetryDelay(error, 1); const delay2 = getRetryDelay(error, 2); expect(delay1).toBe(1000); // 2^1 * 500 expect(delay2).toBe(2000); // 2^2 * 500 }); it('should return exponential backoff for network errors', () => { const error = new AIProviderError('Network', 'NETWORK_ERROR', 'openai'); const delay = getRetryDelay(error, 2); expect(delay).toBe(2000); // 2^2 * 500 }); it('should cap rate limit retry delay at 60 seconds', () => { const error = new AIProviderError('Rate limit', 'RATE_LIMIT_ERROR', 'openai'); const delay = getRetryDelay(error, 10); expect(delay).toBe(60000); }); it('should return exponential backoff for server errors', () => { const error = new AIProviderError('Server error', 'SERVER_ERROR', 'openai'); const delay1 = getRetryDelay(error, 1); const delay2 = getRetryDelay(error, 2); const delay3 = getRetryDelay(error, 3); expect(delay1).toBe(1000); // 2^1 * 500 expect(delay2).toBe(2000); // 2^2 * 500 expect(delay3).toBe(4000); // 2^3 * 500 }); it('should cap other retryable error delays at 30 seconds', () => { const error = new AIProviderError('Timeout', 'TIMEOUT_ERROR', 'openai'); const delay = getRetryDelay(error, 10); expect(delay).toBe(30000); }); it('should return 0 for non-retryable errors', () => { const error = new AIProviderError('Invalid', 'INVALID_REQUEST', 'openai'); const delay = getRetryDelay(error, 1); expect(delay).toBe(0); }); }); });