Node test contract test for a Sume video wrapper, no mock library
Test a Sora-replacement wrapper with node:test and a local http server that answers 202, in_progress, then completed. No network and no key needed.

You can contract-test a Sume video client with Node's built-in test runner and a local http server that returns 202 on submit, in_progress on the first poll and completed on the second. No mocking library is needed, and the test checks the call order: one POST, then two GETs.
Why a local server beats a stubbed fetch
A Sora client written against OpenAI's four statuses usually has a mock that returns queued. A test that never returns pending or cancelled will pass while the real client hangs on the first Sume response. This test uses the statuses Sume documents for /v1/videos.
Node's test runner ships with the runtime, so the test above adds no dependency to your package. It starts a server on a random port, which avoids clashes with anything already listening on your machine or in CI.
The mocked exchange
| Request | Status code | Body status |
|---|---|---|
| POST /v1/videos | 202 | pending |
| First GET of polling_url | 200 | in_progress |
| Second GET of polling_url | 200 | completed, with unsigned_urls |
The client under test
Save this as render.mjs. It submits, polls with a configurable interval and returns the first URL in unsigned_urls. It throws on a non-202 submit and on any terminal state other than completed.
export async function renderClip(base, key, body, pollMs = 30000) {
const h = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };
const res = await fetch(`${base}/v1/videos`, { method: "POST", headers: h, body: JSON.stringify(body) });
if (res.status !== 202) throw new Error(`submit ${res.status}`);
let job = await res.json();
while (!["completed", "failed", "cancelled"].includes(job.status)) {
await new Promise((r) => setTimeout(r, pollMs));
job = await (await fetch(job.polling_url, { headers: h })).json();
}
if (job.status !== "completed") throw new Error(job.error ?? job.status);
return job.unsigned_urls[0];
}The test
Save this as render.test.mjs and run node --test. It builds the polling_url from the server's real port, so the client follows the same link a production job would give it. The poll interval is 5 milliseconds here and 30 seconds as the default.
Assertions on call order matter more than assertions on bodies. The sequence POST, GET, GET proves the wrapper submitted once and polled until the job finished, rather than resubmitting on every loop, which would reserve credits repeatedly in production.
import test from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import { renderClip } from "./render.mjs";
test("submits, polls twice, returns the content url", async () => {
const seen = [];
const server = http.createServer((req, res) => {
seen.push(`${req.method} ${req.url}`);
const port = server.address().port;
const job = { id: "job_1", polling_url: `http://127.0.0.1:${port}/v1/videos/job_1`, model: "wan-3.0" };
res.setHeader("Content-Type", "application/json");
if (req.method === "POST") return res.writeHead(202).end(JSON.stringify({ ...job, status: "pending" }));
const done = seen.filter((s) => s.startsWith("GET")).length > 1;
res.end(JSON.stringify(done ? { ...job, status: "completed", unsigned_urls: [`${job.polling_url}/content?index=0`] } : { ...job, status: "in_progress" }));
});
await new Promise((r) => server.listen(0, r));
const url = await renderClip(`http://127.0.0.1:${server.address().port}`, "k", { model: "wan-3.0", prompt: "x" }, 5);
server.close();
assert.deepEqual(seen.map((s) => s.split(" ")[0]), ["POST", "GET", "GET"]);
assert.match(url, /\/v1\/videos\/job_1\/content\?index=0$/);
});Extend it
Add one test per failure you care about: a failed job with an error string, a cancelled job, and a 402 on submit for insufficient_credits. The error table in the errors docs lists which codes are retryable: 409 job_not_completed is, 409 job_failed is not.
Run it in your CI on every pull request. It finishes in well under a second, and it fails the day someone changes the polling code to treat cancelled as a retryable state.
Keep one live check
Keep one more test outside this file that runs against the real API with a short clip, gated by an environment variable, so the mock cannot drift from the route. The pytest version uses the same idea in Python.
Sources
Related posts
More in Developers
- Omni draft grid: four 360p variants, then one final. What it costs
Google's Draft Room idea, run through the Sume API: four 8-second 360p drafts that change one thing each, then a 1080p final. Total $2.70, with a script.
- One Sume webhook signature, three languages: a shared test vector
A fixed secret, timestamp and body that must sign to the same sume-v1 value in Python, Node and Go. Use it to test a verifier in any language you add.
- Agents SDK cache_tools_list: stale tools after you grant Sume Write
With cache_tools_list on, an OpenAI Agents SDK MCP server can keep an old tool list. After a Sume Write grant, call invalidate_tools_cache() to see paid tools.
- X-OpenRouter-Idempotency-Key vs Sume webhook dedupe on job_id
OpenRouter's webhook dedupe key is job_id plus status. Sume says dedupe on job_id. A SQLite sample builds a job_id plus event key that skips replays.
Written by Sume