node:test mock.method on fetch: test a Sume status poll offline
Node 26 goes LTS this month. Test a Sume job status poll with node:test and mock.method on fetch: four cases, no network, no key, no third-party test library.

Use the built-in test runner: import test, { mock } from "node:test", replace globalThis.fetch with mock.method, return a real Response object, and assert on what your poll function decides. That tests the logic that reads a Sume job status without a network, an API key or a test dependency. Run it with node --test.
The timing is practical. Node 26 moves to LTS in October 2026 according to the Node.js release schedule, so many teams are about to touch their runtime matrix. A poll function with four fast offline tests is a cheap guard for that upgrade. We ran the file below on Node 22.14; it uses only node:test, node:assert and global fetch.
What the tests pin down
The Sume jobs docs describe GET /v1/jobs/{id}/status returning terminal and next_poll_after_seconds, and the latter is null once the job is terminal. The sample function turns that into a decision. The tests then cover the cases that usually regress: a finished job, a queued job with a hint, the request headers, and a non-2xx answer.
| Case | Fake response | Expected decision |
|---|---|---|
| Finished | terminal true, next_poll_after_seconds null | stop polling |
| Queued | terminal false, next_poll_after_seconds 4 | wait 4 seconds |
| Auth | any 200 | one authorization header, never two |
| Rate limited | HTTP 429 | throw, so the caller can read retry-after |
Steps
- Export the function so the test file can import it, and give it a base URL argument.
- Call
mock.method(globalThis, "fetch", async () => new Response(...))at the top of each test. - Build the fake body in the documented envelope:
{ data: { ... } }. - Assert on the decision and, for the header test, on
mock.callsfor the last call. - Run
node --test p18.mjslocally and in CI.
Test file
The auth test matters because the docs say a request must carry one credential, either Authorization: Bearer or x-api-key, not both.
import test, { mock } from "node:test";
import assert from "node:assert/strict";
export async function pollOnce(base, key, id) {
const r = await fetch(`${base}/v1/jobs/${id}/status`, { headers: { authorization: `Bearer ${key}` } });
if (!r.ok) throw new Error(`status ${r.status}`);
const { data } = await r.json();
return data.terminal ? "done" : `wait ${data.next_poll_after_seconds ?? 2}`;
}
const reply = (data, status = 200) => new Response(JSON.stringify({ data }), { status });
test("terminal job", async () => {
mock.method(globalThis, "fetch", async () => reply({ terminal: true }));
assert.equal(await pollOnce("http://x", "k", "j"), "done");
});
test("queued job honors the hint", async () => {
mock.method(globalThis, "fetch", async () => reply({ terminal: false, next_poll_after_seconds: 4 }));
assert.equal(await pollOnce("http://x", "k", "j"), "wait 4");
});
test("sends one auth header", async () => {
const f = mock.method(globalThis, "fetch", async () => reply({ terminal: true }));
await pollOnce("http://x", "k", "j");
assert.deepEqual(Object.keys(f.mock.calls.at(-1).arguments[1].headers), ["authorization"]);
});
test("non-2xx throws", async () => {
mock.method(globalThis, "fetch", async () => reply({}, 429));
await assert.rejects(pollOnce("http://x", "k", "j"), /status 429/);
});Mistakes that make these tests lie
Three habits weaken a mocked poll test. First, returning a plain object instead of a Response, which skips the ok and json() behavior your code really depends on. Second, forgetting to restore the mock, so a later test sees the previous fake; the runner's mock.restoreAll() in an afterEach hook fixes that if you add more files. Third, asserting on the whole request instead of the pieces you care about, which breaks whenever you add a harmless header.
Keep each fixture tiny and shaped like the documented envelope. If the API adds a field, your tests should still pass; if it renames terminal, your smoke call should catch it. That split, offline logic tests plus one live key check, keeps CI fast and honest.
What Sume does not do
Sume does not ship a mock server or a fake clock for polling, and these tests do not prove the live API behaves as the fixtures say. Keep one smoke call to GET /v1/me in a separate job, with a real key, for that. We did not run this file on Node 26 itself.
Sources
Related posts
More in Developers
- npm audit signatures on @sume-com/sdk 0.2.0: what it proves, what not
Run npm audit signatures on @sume-com/sdk 0.2.0 before deploy. It checks the registry signature; the registry lists no provenance attestation for this version.
- Omni Flash API errors: which to retry and which to fix
A retry policy for Gemini Omni Flash 1.1 calls on Sume: 400, 401, 402, 409, 429 rate_limited, 429 queue_full and 503 each mapped to retry, wait or fix.
- Omni job status names: pending vs queued, cancelled vs canceled
Sume's /v1/videos poll uses pending, in_progress, completed, failed and cancelled; /v1/jobs uses queued, processing and canceled. Mapping table for Omni code.
- Omni Flash on /v1/videos vs /v1/video-router: the field map
The same Gemini Omni Flash 1.1 runs on /v1/videos and /v1/video-router/generate with different fields. Field map for frame_images, input_references, video_url.
Written by Sume