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.

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
| Step | Check | Catches |
|---|---|---|
| First poll after 4,999 ms | calls still 1 | Loop polling before the hint |
| Tick the last 1 ms | calls becomes 2 | Loop ignoring the hint entirely |
| Hint is null | 2,000 ms wait | Missing fallback of any kind |
| terminal true | Returns the body | Loop 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
- One length gate for six ad platforms: caps table and Sume plan
Reels 15 min, Stories 60 min, TikTok 10 min, LinkedIn 30 min, Pinterest 5 min, Snap 180 s. Check one Sume Timeline length against all six with the plan call.
- openapi-fetch with Sume's OpenAPI JSON: a typed client in 20 lines
Generate types from Sume's reference JSON with openapi-typescript, then call it with openapi-fetch and an x-api-key middleware. A type-checked sample.
- OpenRouter: poll video every 30 s. Sume: obey next_poll_after_seconds
OpenRouter recommends a 30 second poll on video. Sume's jobs API returns next_poll_after_seconds, else backoff. A Python poller that does both.
- OpenRouter's video expired event has no Sume twin: one normalizer
OpenRouter sends completed, failed, cancelled and expired video events; Sume sends three job events. A Python normalizer and verifier for both.
Written by Sume