76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
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<void>>()
|
|
.mockImplementationOnce(() => new Promise<void>((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<void>>().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);
|
|
});
|
|
});
|