import { describe, it, expect, vi } from 'vitest'; import { withRetry } from '../src/utils/retry.js'; import { createMockLogger } from '../tests/mockLogger.js'; describe('withRetry', () => { it('succeeds on first attempt', async () => { const fn = vi.fn(() => Promise.resolve('success')); const result = await withRetry(fn, { retries: 3, minTimeout: 0 }); expect(result).toBe('success'); expect(fn).toHaveBeenCalledTimes(1); }); it('retries on failure then succeeds', async () => { let attempts = 0; const fn = vi.fn(() => { attempts++; if (attempts < 3) return Promise.reject(new Error(`Attempt ${attempts} failed`)); return Promise.resolve('success'); }); const result = await withRetry(fn, { retries: 3, minTimeout: 0 }); expect(result).toBe('success'); expect(attempts).toBe(3); }); it('throws after all retries exhausted', async () => { const fn = vi.fn(() => Promise.reject(new Error('permanent failure'))); await expect(withRetry(fn, { retries: 2, minTimeout: 0 })).rejects.toThrow('permanent failure'); expect(fn).toHaveBeenCalledTimes(3); // 1 initial + 2 retries }); it('logs warnings on failed attempts', async () => { const logger = createMockLogger(); let attempts = 0; const fn = vi.fn(() => { attempts++; return Promise.reject(new Error(`fail ${attempts}`)); }); await withRetry(fn, { retries: 2, logger, operationName: 'test-op', minTimeout: 0, }).catch(() => {}); const warnLogs = logger.logs.filter(l => l.level === 'warn'); // 1 initial + 2 retries = 3 failed attempts, each logs a warning. expect(warnLogs.length).toBe(3); expect(warnLogs[0].message).toContain('test-op'); expect(warnLogs[0].message).toContain('fail 1'); expect(warnLogs[1].message).toContain('fail 2'); }); it('uses default operation name', async () => { const logger = createMockLogger(); const fn = vi.fn(() => Promise.reject(new Error('fail'))); await withRetry(fn, { logger, retries: 1, minTimeout: 0 }).catch(() => {}); const warnLogs = logger.logs.filter(l => l.level === 'warn'); expect(warnLogs[0].message).toContain('operation'); }); it('supports custom retry count', async () => { const fn = vi.fn(() => Promise.reject(new Error('fail'))); await withRetry(fn, { retries: 5, minTimeout: 0 }).catch(() => {}); expect(fn).toHaveBeenCalledTimes(6); // 1 initial + 5 retries }); it('handles non-Error rejections', async () => { const fn = vi.fn(() => Promise.reject('string error')); // p-retry wraps non-Error rejections in a TypeError. The wrap is treated // as an abort (no retry), so only the initial attempt runs. await expect(withRetry(fn, { retries: 1, minTimeout: 0 })).rejects.toThrow('string error'); expect(fn).toHaveBeenCalledTimes(1); }); it('passes options to retry correctly', async () => { const logger = createMockLogger(); let attempts = 0; const fn = vi.fn(() => { attempts++; return Promise.reject(new Error('fail')); }); await withRetry(fn, { retries: 1, logger, operationName: 'custom-op', minTimeout: 0, }).catch(() => {}); expect(attempts).toBe(2); expect(logger.logs.some(l => l.message.includes('custom-op'))).toBe(true); }); });