/**
* Tests for retry handler
*/
import { describe, it, expect, vi } from 'vitest';
import { withRetry, createRetryHandler, RetryableErrors } from './retry.js';
describe('withRetry', () => {
it('should return result on first success', async () => {
const fn = vi.fn().mockResolvedValue('success');
const result = await withRetry(fn);
expect(result).toBe('success');
expect(fn).toHaveBeenCalledTimes(1);
});
it('should retry on failure and succeed', async () => {
const fn = vi
.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValue('success');
const result = await withRetry(fn, {
maxAttempts: 3,
initialDelayMs: 10,
});
expect(result).toBe('success');
expect(fn).toHaveBeenCalledTimes(2);
});
it('should throw after max attempts', async () => {
const fn = vi.fn().mockRejectedValue(new Error('always fail'));
await expect(
withRetry(fn, {
maxAttempts: 3,
initialDelayMs: 10,
})
).rejects.toThrow('always fail');
expect(fn).toHaveBeenCalledTimes(3);
});
it('should not retry non-retryable errors', async () => {
const fn = vi.fn().mockRejectedValue(new Error('not retryable'));
await expect(
withRetry(fn, {
maxAttempts: 3,
isRetryable: () => false,
})
).rejects.toThrow('not retryable');
expect(fn).toHaveBeenCalledTimes(1);
});
it('should call onRetry callback', async () => {
const fn = vi
.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValue('success');
const onRetry = vi.fn();
await withRetry(fn, {
maxAttempts: 3,
initialDelayMs: 10,
onRetry,
});
expect(onRetry).toHaveBeenCalledTimes(1);
expect(onRetry).toHaveBeenCalledWith(expect.any(Error), 1, expect.any(Number));
});
});
describe('createRetryHandler', () => {
it('should create a reusable retry wrapper', async () => {
const handler = createRetryHandler({
maxAttempts: 2,
initialDelayMs: 10,
});
const fn = vi.fn().mockResolvedValue('result');
const result = await handler(fn);
expect(result).toBe('result');
});
});
describe('RetryableErrors', () => {
describe('isNetworkError', () => {
it('should detect network errors', () => {
expect(RetryableErrors.isNetworkError(new Error('ECONNRESET'))).toBe(true);
expect(RetryableErrors.isNetworkError(new Error('ETIMEDOUT'))).toBe(true);
expect(RetryableErrors.isNetworkError(new Error('regular error'))).toBe(false);
});
});
describe('isServerError', () => {
it('should detect 5xx errors', () => {
const err500 = Object.assign(new Error('Server error'), { status: 500 });
const err404 = Object.assign(new Error('Not found'), { status: 404 });
expect(RetryableErrors.isServerError(err500)).toBe(true);
expect(RetryableErrors.isServerError(err404)).toBe(false);
});
});
describe('isRateLimitError', () => {
it('should detect 429 errors', () => {
const err429 = Object.assign(new Error('Rate limited'), { status: 429 });
const err500 = Object.assign(new Error('Server error'), { status: 500 });
expect(RetryableErrors.isRateLimitError(err429)).toBe(true);
expect(RetryableErrors.isRateLimitError(err500)).toBe(false);
});
});
});