import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as errors from '../ts/errors/index.js'; import { PlatformError, ValidationError, NetworkError, ResourceError, OperationError } from '../ts/errors/base.errors.js'; import { ErrorSeverity, ErrorCategory, ErrorRecoverability } from '../ts/errors/error.codes.js'; import { ErrorHandler } from '../ts/errors/error-handler.js'; // Test base error classes tap.test('Base error classes should set properties correctly', async () => { const message = 'Test error message'; const code = 'TEST_ERROR_CODE'; const context = { component: 'TestComponent', operation: 'testOperation', data: { foo: 'bar' } }; // Test PlatformError const platformError = new PlatformError( message, code, ErrorSeverity.MEDIUM, ErrorCategory.OPERATION, ErrorRecoverability.MAYBE_RECOVERABLE, context ); expect(platformError.message).toEqual(message); expect(platformError.code).toEqual(code); expect(platformError.severity).toEqual(ErrorSeverity.MEDIUM); expect(platformError.category).toEqual(ErrorCategory.OPERATION); expect(platformError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE); expect(platformError.context?.component).toEqual(context.component); expect(platformError.context?.operation).toEqual(context.operation); expect(platformError.context?.data?.foo).toEqual('bar'); expect(platformError.name).toEqual('PlatformError'); // Test ValidationError const validationError = new ValidationError(message, code, context); expect(validationError.category).toEqual(ErrorCategory.VALIDATION); expect(validationError.severity).toEqual(ErrorSeverity.LOW); // Test NetworkError const networkError = new NetworkError(message, code, context); expect(networkError.category).toEqual(ErrorCategory.CONNECTIVITY); expect(networkError.severity).toEqual(ErrorSeverity.MEDIUM); expect(networkError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE); // Test ResourceError const resourceError = new ResourceError(message, code, context); expect(resourceError.category).toEqual(ErrorCategory.RESOURCE); }); // Test error handler utility tap.test('ErrorHandler should properly handle and format errors', async () => { // Configure error handler ErrorHandler.configure({ logErrors: false, // Disable for testing includeStacksInProd: false, retry: { maxAttempts: 5, baseDelay: 100, maxDelay: 1000, backoffFactor: 2 } }); // Test converting regular Error to PlatformError const regularError = new Error('Something went wrong'); const platformError = ErrorHandler.toPlatformError( regularError, 'PLATFORM_OPERATION_ERROR', { component: 'TestHandler' } ); expect(platformError).toBeInstanceOf(PlatformError); expect(platformError.code).toEqual('PLATFORM_OPERATION_ERROR'); expect(platformError.context?.component).toEqual('TestHandler'); // Test formatting error for API response const formattedError = ErrorHandler.formatErrorForResponse(platformError, true); expect(formattedError.code).toEqual('PLATFORM_OPERATION_ERROR'); expect(formattedError.message).toEqual('An unexpected error occurred.'); expect(formattedError.details?.rawMessage).toEqual('Something went wrong'); // Test executing a function with error handling let executed = false; try { await ErrorHandler.execute(async () => { executed = true; throw new Error('Execution failed'); }, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' }); } catch (error) { expect(error).toBeInstanceOf(PlatformError); expect(error.code).toEqual('TEST_EXECUTION_ERROR'); expect(error.context.operation).toEqual('testExecution'); } expect(executed).toEqual(true); // Test executeWithRetry successful after retries let attempts = 0; const result = await ErrorHandler.executeWithRetry( async () => { attempts++; if (attempts < 3) { throw new Error('Temporary failure'); } return 'success'; }, 'TEST_RETRY_ERROR', { maxAttempts: 5, baseDelay: 10, // Use small delay for tests retryableErrorPatterns: [/Temporary failure/], // Add pattern to make error retryable onRetry: (error, attempt, delay) => { expect(error).toBeInstanceOf(PlatformError); expect(attempt).toBeGreaterThan(0); expect(delay).toBeGreaterThan(0); } } ); expect(result).toEqual('success'); expect(attempts).toEqual(3); // Test executeWithRetry that fails after max attempts attempts = 0; try { await ErrorHandler.executeWithRetry( async () => { attempts++; throw new Error('Persistent failure'); }, 'TEST_RETRY_ERROR', { maxAttempts: 3, baseDelay: 10, retryableErrorPatterns: [/Persistent failure/] // Make error retryable so it tries all attempts } ); } catch (error) { expect(error).toBeInstanceOf(PlatformError); expect(attempts).toEqual(3); } }); // Test retry utilities tap.test('Error retry utilities should work correctly', async () => { let attempts = 0; try { await errors.retry( async () => { attempts++; if (attempts < 3) { throw new Error('Temporary error'); } return 'success'; }, { maxRetries: 5, initialDelay: 20, backoffFactor: 1.5, retryableErrors: [/Temporary/] } ); } catch (e) { // Should not reach here expect(false).toEqual(true); } expect(attempts).toEqual(3); // Test retry with non-retryable error attempts = 0; try { await errors.retry( async () => { attempts++; throw new Error('Critical error'); }, { maxRetries: 3, initialDelay: 10, retryableErrors: [/Temporary/] // Won't match "Critical" } ); } catch (error) { expect(error.message).toEqual('Critical error'); expect(attempts).toEqual(1); // Should only attempt once } }); // Helper function that will reject first n times, then resolve interface FlakyFunction { (failTimes: number, result?: any): Promise; counter: number; reset: () => void; } const flaky: FlakyFunction = Object.assign( async function (failTimes: number, result: any = 'success'): Promise { if (flaky.counter < failTimes) { flaky.counter++; throw new Error(`Flaky failure ${flaky.counter}`); } return result; }, { counter: 0, reset: () => { flaky.counter = 0; } } ); // Test error wrapping and retry combination tap.test('Error handling can be combined with retry for robust operations', async () => { // Reset counter for the test flaky.reset(); // Create a wrapped version of the flaky function const wrapped = errors.withErrorHandling( () => flaky(2, 'wrapped success'), 'TEST_WRAPPED_ERROR', { component: 'TestComponent' } ); // Execute with retry const result = await errors.retry( wrapped, { maxRetries: 3, initialDelay: 10, retryableErrors: [/Flaky failure/] } ); expect(result).toEqual('wrapped success'); expect(flaky.counter).toEqual(2); // Reset and test failure case flaky.reset(); try { await errors.retry( () => flaky(5, 'never reached'), { maxRetries: 2, // Only retry twice, but we need 5 attempts to succeed initialDelay: 10, retryableErrors: [/Flaky failure/] // Add pattern to make it retry } ); // Should not reach here expect(false).toEqual(true); } catch (error) { expect(error.message).toContain('Flaky failure'); expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts } }); tap.test('stop', async () => { await tap.stopForcefully(); }); export default tap.start();