import { describe, expect, it } from "@suaveplan/testing/runner"; import { CronScheduler } from "../src/index.js"; import type { SchedulerTimers } from "../src/scheduler/cron-scheduler.types.js"; interface PendingTimer { id: number; cb: () => void; fireAt: number; } function createHarness(start: number) { const state = { now: start, nextId: 1 }; const pending = new Map(); const timers: SchedulerTimers = { setTimer: (cb, ms) => { const id = state.nextId++; pending.set(id, { id, cb, fireAt: state.now + ms }); return id; }, clearTimer: (id) => pending.delete(id), }; async function advance(delta: number): Promise { const target = state.now + delta; while (true) { let earliest: PendingTimer | null = null; for (const t of pending.values()) { if (t.fireAt > target) continue; if (earliest === null || t.fireAt < earliest.fireAt) earliest = t; } if (earliest === null) break; state.now = earliest.fireAt; pending.delete(earliest.id); earliest.cb(); // Let micro-task queue drain so the handler's finally re-schedules // before we look for the next due timer. await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); } state.now = target; } return { clock: () => state.now, timers, advance, get pending() { return pending; }, }; } describe("integration — CronScheduler with multiple jobs", () => { it("fires three jobs at their independent intervals", { timeout: 30000, }, async () => { const harness = createHarness(Date.UTC(2026, 4, 11, 9, 59, 0)); const fires = { hourly: 0, fiveMin: 0, daily: 0 }; const scheduler = new CronScheduler({ clock: harness.clock, timers: harness.timers, }); scheduler.add( "0 * * * *", () => { fires.hourly += 1; }, { id: "hourly" } ); scheduler.add( "*/5 * * * *", () => { fires.fiveMin += 1; }, { id: "five" } ); scheduler.add( "0 0 * * *", () => { fires.daily += 1; }, { id: "daily" } ); scheduler.start(); // Advance 2 hours and 30 minutes. await harness.advance(2 * 60 * 60 * 1000 + 30 * 60 * 1000); expect(fires.hourly).toBeGreaterThanOrEqual(2); expect(fires.fiveMin).toBeGreaterThanOrEqual(20); await scheduler.stop(); }); it("stop() drains in-flight handler", { timeout: 30000 }, async () => { const harness = createHarness(Date.UTC(2026, 4, 11, 9, 59, 0)); let resolveHandler!: () => void; const holding = new Promise((r) => { resolveHandler = r; }); let completed = false; const scheduler = new CronScheduler({ clock: harness.clock, timers: harness.timers, }); scheduler.add("0 * * * *", async () => { await holding; completed = true; }); scheduler.start(); await harness.advance(2 * 60 * 1000); const stopPromise = scheduler.stop(); let stopResolved = false; const watcher = stopPromise.then(() => { stopResolved = true; }); // Give the runtime a few microtasks to let the watcher attach. await Promise.resolve(); await Promise.resolve(); expect(stopResolved).toBe(false); resolveHandler(); await watcher; expect(completed).toBe(true); expect(stopResolved).toBe(true); }); });