375 lines
10 KiB
TypeScript
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('');
|
|
});
|
|
});
|
|
});
|