text-processing/text-utils/src/errors/error-handler.test.ts

375 lines
10 KiB
TypeScript

import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import { ErrorHandler, ErrorAggregator } from './error-handler.js';
import { TextProcessingError } from './text-error.js';
describe('ErrorHandler', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
describe('handle (async)', () => {
test('should return result on success', async () => {
const handler = new ErrorHandler();
const result = await handler.handle(async () => 'success');
expect(result).toBe('success');
});
test('should retry on failure', async () => {
const handler = new ErrorHandler({ maxRetries: 3, retryDelay: 100 });
let attempts = 0;
const promise = handler.handle(async () => {
attempts++;
if (attempts < 3) {
throw new Error('retry me');
}
return 'success';
});
// Fast-forward through retries
await vi.advanceTimersByTimeAsync(200);
const result = await promise;
expect(result).toBe('success');
expect(attempts).toBe(3);
});
test('should throw after max retries', async () => {
vi.useRealTimers();
const handler = new ErrorHandler({ maxRetries: 2, retryDelay: 10 });
await expect(
handler.handle(async () => {
throw new Error('always fails');
}),
).rejects.toThrow('always fails');
});
test('should call onError callback', async () => {
vi.useRealTimers();
const onError = vi.fn();
const handler = new ErrorHandler({ maxRetries: 0, onError });
try {
await handler.handle(async () => {
throw new Error('test error');
});
} catch {
// Expected
}
expect(onError).toHaveBeenCalled();
});
test('should throw immediately on fatal errors', async () => {
const handler = new ErrorHandler({ maxRetries: 3 });
await expect(
handler.handle(async () => {
throw new TextProcessingError('Validation failed', 'VALIDATION_ERROR');
}),
).rejects.toThrow();
});
});
describe('handleSync', () => {
test('should return result on success', () => {
const handler = new ErrorHandler();
const result = handler.handleSync(() => 'success');
expect(result).toBe('success');
});
test('should throw on error (no retries)', () => {
const handler = new ErrorHandler();
expect(() => {
handler.handleSync(() => {
throw new Error('sync error');
});
}).toThrow('sync error');
});
test('should call onError callback', () => {
const onError = vi.fn();
const handler = new ErrorHandler({ onError });
try {
handler.handleSync(() => {
throw new Error('test');
});
} catch {
// Expected
}
expect(onError).toHaveBeenCalled();
});
});
describe('exponential backoff', () => {
test('should increase delay exponentially', async () => {
vi.useRealTimers();
const handler = new ErrorHandler({
maxRetries: 2,
retryDelay: 10,
exponentialBackoff: true,
});
let attempts = 0;
await expect(
handler.handle(async () => {
attempts++;
throw new Error('retry');
}),
).rejects.toThrow();
expect(attempts).toBe(3); // Initial + 2 retries
});
});
describe('wrap', () => {
test('should wrap sync function', () => {
const handler = new ErrorHandler();
const fn = (x: number) => x * 2;
const wrapped = handler.wrap(fn);
expect(wrapped(5)).toBe(10);
});
test('should wrap throwing sync function', () => {
const handler = new ErrorHandler();
const fn = () => {
throw new Error('wrapped error');
};
const wrapped = handler.wrap(fn);
expect(() => wrapped()).toThrow('wrapped error');
});
});
describe('createRetryable', () => {
test('should create retryable function', async () => {
let attempts = 0;
const retryable = ErrorHandler.createRetryable(
async () => {
attempts++;
if (attempts < 2) throw new Error('retry');
return 'done';
},
{ maxRetries: 3, retryDelay: 10 },
);
const promise = retryable();
await vi.advanceTimersByTimeAsync(20);
const result = await promise;
expect(result).toBe('done');
});
});
describe('withTimeout', () => {
test('should return result within timeout', async () => {
const result = await ErrorHandler.withTimeout(
async () => {
return 'fast';
},
1000,
);
expect(result).toBe('fast');
});
test('should throw on timeout', async () => {
const promise = ErrorHandler.withTimeout(async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
return 'slow';
}, 100);
vi.advanceTimersByTime(200);
await expect(promise).rejects.toThrow(/timed out/);
});
test('should use custom timeout error', async () => {
const customError = new Error('Custom timeout');
const promise = ErrorHandler.withTimeout(
async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
},
100,
customError,
);
vi.advanceTimersByTime(200);
await expect(promise).rejects.toThrow('Custom timeout');
});
});
describe('retry count tracking', () => {
test('should track retry count', async () => {
vi.useRealTimers();
const handler = new ErrorHandler({ maxRetries: 1, retryDelay: 10 });
try {
await handler.handle(
async () => {
throw new Error('fail');
},
'test-context',
);
} catch {
// Expected
}
// After failure, retry count should be reset or at max
expect(handler.getRetryCount('test-context')).toBeGreaterThanOrEqual(0);
});
test('should reset retry count', () => {
const handler = new ErrorHandler({ maxRetries: 1, retryDelay: 10 });
handler.resetRetryCount('test');
expect(handler.getRetryCount('test')).toBe(0);
});
test('should clear all retry counts', () => {
const handler = new ErrorHandler();
handler.resetRetryCount();
expect(handler.getRetryCount('any')).toBe(0);
});
});
});
describe('ErrorAggregator', () => {
describe('basic operations', () => {
test('should add errors', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('error1'));
aggregator.add(new Error('error2'));
expect(aggregator.getErrors()).toHaveLength(2);
});
test('should add errors with context', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('error'), { file: 'test.ts', line: 42 });
const errors = aggregator.getErrors();
expect(errors[0].context).toEqual({ file: 'test.ts', line: 42 });
});
test('should track timestamp', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('error'));
const errors = aggregator.getErrors();
expect(errors[0].timestamp).toBeInstanceOf(Date);
});
});
describe('hasErrors', () => {
test('should return false when empty', () => {
const aggregator = new ErrorAggregator();
expect(aggregator.hasErrors()).toBe(false);
});
test('should return true with errors', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('error'));
expect(aggregator.hasErrors()).toBe(true);
});
});
describe('getErrorsByType', () => {
test('should filter by error type', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new TypeError('type error'));
aggregator.add(new RangeError('range error'));
aggregator.add(new TypeError('another type error'));
const typeErrors = aggregator.getErrorsByType(TypeError);
expect(typeErrors).toHaveLength(2);
});
test('should return empty array if no matches', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('generic'));
const syntaxErrors = aggregator.getErrorsByType(SyntaxError);
expect(syntaxErrors).toHaveLength(0);
});
});
describe('hasErrorType', () => {
test('should detect specific error type', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new TypeError('oops'));
expect(aggregator.hasErrorType(TypeError)).toBe(true);
expect(aggregator.hasErrorType(RangeError)).toBe(false);
});
});
describe('clear', () => {
test('should remove all errors', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('error1'));
aggregator.add(new Error('error2'));
aggregator.clear();
expect(aggregator.hasErrors()).toBe(false);
expect(aggregator.getErrors()).toHaveLength(0);
});
});
describe('throwIfErrors', () => {
test('should not throw when empty', () => {
const aggregator = new ErrorAggregator();
expect(() => aggregator.throwIfErrors()).not.toThrow();
});
test('should throw single error directly', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('single error'));
expect(() => aggregator.throwIfErrors()).toThrow('single error');
});
test('should throw AggregateError for multiple errors', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new Error('error1'));
aggregator.add(new Error('error2'));
expect(() => aggregator.throwIfErrors()).toThrow(AggregateError);
});
});
describe('getSummary', () => {
test('should summarize error counts', () => {
const aggregator = new ErrorAggregator();
aggregator.add(new TypeError('type1'));
aggregator.add(new TypeError('type2'));
aggregator.add(new RangeError('range1'));
const summary = aggregator.getSummary();
expect(summary).toContain('TypeError: 2');
expect(summary).toContain('RangeError: 1');
});
test('should handle empty aggregator', () => {
const aggregator = new ErrorAggregator();
const summary = aggregator.getSummary();
expect(summary).toBe('');
});
});
});