import { describe, expect, it, vi } from 'vitest'; import { calculateCandidateFunnelTarget, currentHourlySendCount, startNonOverlappingSchedule, } from '../src/main-loop.js'; describe('startNonOverlappingSchedule', () => { it('does not begin a second response check until the prior check settles', async () => { vi.useFakeTimers(); let finishFirst: (() => void) | undefined; const check = vi .fn<() => Promise>() .mockImplementationOnce(() => new Promise((resolve) => { finishFirst = resolve; })) .mockResolvedValue(undefined); const schedule = startNonOverlappingSchedule(check, 1_000); expect(check).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(10_000); expect(check).toHaveBeenCalledTimes(1); finishFirst?.(); await Promise.resolve(); await vi.advanceTimersByTimeAsync(1_000); expect(check).toHaveBeenCalledTimes(2); schedule.stop(); vi.useRealTimers(); }); it('does not schedule another check after stop', async () => { vi.useFakeTimers(); const check = vi.fn<() => Promise>().mockResolvedValue(undefined); const schedule = startNonOverlappingSchedule(check, 1_000); await Promise.resolve(); schedule.stop(); await vi.advanceTimersByTimeAsync(10_000); expect(check).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); }); describe('currentHourlySendCount', () => { it('uses the post-send limiter count without adding an extra message', () => { expect(currentHourlySendCount({ getStats: () => ({ messagesThisHour: 1 }) })).toBe(1); }); }); describe('calculateCandidateFunnelTarget', () => { it('tracks remaining outbound capacity instead of growing to a fixed backlog', () => { expect(calculateCandidateFunnelTarget( [{ messagesRemaining: 3 }, { messagesRemaining: 5 }], { maxReadyCandidates: 25, recentWindowMinutes: 60, minDelayBetweenMessagesMs: 300_000 }, )).toBe(8); }); it('retains the configured ceiling and caps capacity by the freshness window', () => { expect(calculateCandidateFunnelTarget( [{ messagesRemaining: 100 }, { messagesRemaining: 100 }], { maxReadyCandidates: 25, recentWindowMinutes: 30, minDelayBetweenMessagesMs: 300_000 }, )).toBe(12); }); it('stops growing inventory when no running account can send', () => { const options = { maxReadyCandidates: 25, recentWindowMinutes: 60, minDelayBetweenMessagesMs: 300_000, }; expect(calculateCandidateFunnelTarget([], options)).toBe(0); expect(calculateCandidateFunnelTarget([{ messagesRemaining: 0 }], options)).toBe(0); }); });