import { describe, it, expect } from 'vitest'; import { retryWithBackoff, RetryError } from '../../src/utils/retry'; describe('retryWithBackoff', () => { it('succeeds on first attempt without retrying', async () => { let callCount = 0; const result = await retryWithBackoff(async () => { callCount++; return 'ok'; }, { maxRetries: 3, initialDelayMs: 1, jitter: false }); expect(result).toBe('ok'); expect(callCount).toBe(1); }); it('retries and succeeds on 2nd attempt', async () => { let callCount = 0; const result = await retryWithBackoff(async () => { callCount++; if (callCount < 2) throw new Error('transient'); return 'recovered'; }, { maxRetries: 3, initialDelayMs: 1, jitter: false }); expect(result).toBe('recovered'); expect(callCount).toBe(2); }); it('throws RetryError after all attempts exhausted', async () => { let callCount = 0; await expect( retryWithBackoff(async () => { callCount++; throw new Error('permanent'); }, { maxRetries: 2, initialDelayMs: 1, jitter: false }) ).rejects.toThrow(RetryError); // 1 initial + 2 retries = 3 total expect(callCount).toBe(3); }); it('respects maxRetries option', async () => { let callCount = 0; await expect( retryWithBackoff(async () => { callCount++; throw new Error('fail'); }, { maxRetries: 1, initialDelayMs: 1, jitter: false }) ).rejects.toThrow(RetryError); // 1 initial + 1 retry = 2 total expect(callCount).toBe(2); }); it('RetryError contains attempt count and last error', async () => { try { await retryWithBackoff(async () => { throw new Error('specific failure'); }, { maxRetries: 2, initialDelayMs: 1, jitter: false }); } catch (error) { expect(error).toBeInstanceOf(RetryError); const retryErr = error as RetryError; expect(retryErr.attempts).toBe(3); expect(retryErr.lastError.message).toBe('specific failure'); } }); });