Test a Sume poll loop with node:test mock.timers, no real sleeping

Use node:test mock.timers to prove your poll loop waits next_poll_after_seconds, falls back to 2 s, and never polls early. A runnable test, no network.

4 min readSume
All posts

Call t.mock.timers.enable({ apis: ["setTimeout"] }) inside a node:test test, then advance time with t.mock.timers.tick(ms). The Node test docs (read 2026-10-10) list MockTimers with enable, tick, runAll, setTime and reset, and the apis option picks which of setTimeout, setInterval, setImmediate and Date to fake.

A Sume poll loop sleeps whatever the status body asks for in next_poll_after_seconds, so a test that really waits is slow and a test that never checks the delay misses the bug that matters.

The function under test

The loop below is the part worth testing: it asks for status, stops when terminal is true, and otherwise sleeps the hint, or 2 seconds when the hint is missing. The Jobs and results page says to obey the hint when it is present and to back off when it is not; a flat 2 seconds is this sample's simple fallback.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function waitForJob(id, getStatus, maxPolls = 20) {
  for (let i = 0; i < maxPolls; i++) {
    const s = await getStatus(id);
    if (s.terminal) return s;
    await sleep((s.next_poll_after_seconds ?? 2) * 1000);
  }
  throw new Error(`job ${id} still running after ${maxPolls} polls`);
}

The test, with one trap handled

Only setTimeout is mocked. If you also mock setImmediate, the helper that lets the promise queue run never fires, and the test hangs. The helper flush uses the real setImmediate to yield after each tick so the awaiting loop gets to call the fake status function again.

import assert from "node:assert/strict";
import { test } from "node:test";
import { waitForJob } from "./wait-for-job.mjs";

const flush = () => new Promise((resolve) => setImmediate(resolve));

test("sleeps next_poll_after_seconds, falls back to 2 s", async (t) => {
  t.mock.timers.enable({ apis: ["setTimeout"] });
  const replies = [
    { terminal: false, next_poll_after_seconds: 5 },
    { terminal: false, next_poll_after_seconds: null },
    { terminal: true, sume_status: "completed" },
  ];
  let calls = 0;
  const done = waitForJob("job_1", async () => replies[calls++]);

  await flush();
  assert.equal(calls, 1);
  t.mock.timers.tick(4_999);
  await flush();
  assert.equal(calls, 1); // 1 ms early: no second poll yet
  t.mock.timers.tick(1);
  await flush();
  assert.equal(calls, 2);
  t.mock.timers.tick(2_000); // null hint -> 2 s default
  await flush();
  assert.equal((await done).sume_status, "completed");
});

What the test proves and what it does not

Assertions in the test (Node docs read 2026-10-10)
StepCheckCatches
First poll after 4,999 mscalls still 1Loop polling before the hint
Tick the last 1 mscalls becomes 2Loop ignoring the hint entirely
Hint is null2,000 ms waitMissing fallback of any kind
terminal trueReturns the bodyLoop that spins past the end

Run it

Save both files, then run node --test. On Node 22 and newer the test needs no flags. Because no real time passes, it finishes in a few milliseconds, which keeps it in the fast unit layer of your suite.

Two details are worth copying into your own tests. First, assert on call counts at the exact millisecond boundary, 4,999 and then 1 more, because off-by-one waits are the common bug. Second, call reset or let the test context do it, so the fake clock never leaks into the next test file.

The test does not prove Sume's hint values; for that you need the dev host. It proves your code obeys any hint it receives, and that is the contract your loop owns.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume