import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { startIntervalRunner } from "./interval-runner.ts"; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); /** A run that stays in flight until the test lets it go. */ function heldRun() { let release!: () => void; let finished = false; const run = vi.fn( () => new Promise((resolve) => { release = () => { finished = true; resolve(); }; }), ); return { run, release: () => release(), finished: () => finished }; } describe("startIntervalRunner", () => { it("never overlaps two ticks", async () => { const held = heldRun(); const handle = startIntervalRunner({ name: "test", tickMs: 100, run: held.run }); await vi.advanceTimersByTimeAsync(350); expect(held.run).toHaveBeenCalledTimes(1); held.release(); await handle.stop(); }); it("waits on the tick still running when the interval fired during it", async () => { const held = heldRun(); const handle = startIntervalRunner({ name: "test", tickMs: 100, run: held.run }); // Several interval fires land while the first tick is still running. await vi.advanceTimersByTimeAsync(350); let stopped = false; const stopping = handle.stop(5_000).then(() => { stopped = true; }); await vi.advanceTimersByTimeAsync(10); expect(stopped).toBe(false); held.release(); await stopping; expect(held.finished()).toBe(true); }); it("gives up waiting after the timeout", async () => { const held = heldRun(); const handle = startIntervalRunner({ name: "test", tickMs: 100, run: held.run }); const stopping = handle.stop(2_000); await vi.advanceTimersByTimeAsync(2_000); await stopping; expect(held.finished()).toBe(false); }); it("starts no tick after stop", async () => { const run = vi.fn().mockResolvedValue(undefined); const handle = startIntervalRunner({ name: "test", tickMs: 100, run }); await handle.stop(); await vi.advanceTimersByTimeAsync(500); expect(run).toHaveBeenCalledTimes(1); }); });