import { describe, expect, it, vi, afterEach } from 'vitest'; import { createLimiter, extractionLimiter } from '../llmRetry'; afterEach(() => { vi.useRealTimers(); }); describe('createLimiter', () => { it('rejects an invalid rps', () => { expect(() => createLimiter({ rps: 0 })).toThrow(); expect(() => createLimiter({ rps: -1 })).toThrow(); }); it('rejects an invalid burst', () => { expect(() => createLimiter({ rps: 1, burst: 0 })).toThrow(); }); it('lets the first call through immediately (initial burst token)', async () => { const limiter = createLimiter({ rps: 1, burst: 1 }); const start = Date.now(); await limiter.acquire(); expect(Date.now() - start).toBeLessThan(50); }); it('queues subsequent calls to respect the spacing', async () => { vi.useFakeTimers(); const limiter = createLimiter({ rps: 10, burst: 1 }); // 100ms spacing await limiter.acquire(); // consume initial token let resolved = false; const p = limiter.acquire().then(() => { resolved = true; }); await vi.advanceTimersByTimeAsync(50); expect(resolved).toBe(false); await vi.advanceTimersByTimeAsync(100); await p; expect(resolved).toBe(true); }); it('honours pauseUntil — no acquire returns before the pause expires', async () => { vi.useFakeTimers(); const limiter = createLimiter({ rps: 100, burst: 5 }); limiter.pauseUntil(Date.now() + 1000); let resolved = false; const p = limiter.acquire().then(() => { resolved = true; }); await vi.advanceTimersByTimeAsync(500); expect(resolved).toBe(false); await vi.advanceTimersByTimeAsync(600); await p; expect(resolved).toBe(true); }); it('aborts a queued acquire when the signal fires', async () => { const limiter = createLimiter({ rps: 1, burst: 1 }); await limiter.acquire(); // consume const ctl = new AbortController(); const p = limiter.acquire({ signal: ctl.signal }); ctl.abort(); await expect(p).rejects.toBeInstanceOf(DOMException); }); }); describe('extractionLimiter', () => { it('is exported and exposes the limiter shape', () => { expect(typeof extractionLimiter.acquire).toBe('function'); expect(typeof extractionLimiter.pauseUntil).toBe('function'); }); });