Deno 2.9 t.assertSnapshot: contract-test the Sume status envelope
Deno 2.9 builds assertSnapshot into the test context. Snapshot the field names and types of GET /v1/jobs/:id/status for a completed job to catch API drift.

Short answer
Fetch the status of one completed Sume job, reduce the data object to field names and value types, and call await t.assertSnapshot(shape). Deno 2.9 built t.assertSnapshot() into the test context with no import, storing snapshots in __snapshots__/<test file>.snap and updating them with deno test --update-snapshots.
The purpose is a cheap contract test: if the status envelope gains or loses a field your poller reads, the snapshot diff shows it before production does. Sume's own docs say to treat the live OpenAPI schema as the exact source, and this test is a local tripwire next to it.
What Deno 2.9 documents
The release post is dated 25 June 2026.
| Item | Detail |
|---|---|
| t.assertSnapshot() | available on the test context without an import |
| Storage | __snapshots__/<test file>.snap beside the test |
| Update | deno test --update-snapshots |
| Permissions for this test | --allow-net and --allow-env |
Why a completed job
Pin the test to a job that is already completed. While a job runs, next_poll_after_seconds is a number; once terminal it is null, so the type flips with the state. A terminal job gives a stable shape, and a second snapshot for a running job is a separate test.
| Field | Running | Terminal |
|---|---|---|
| terminal | false | true |
| result_ready | false | true when completed |
| next_poll_after_seconds | number | null |
| cancel_url | string | null |
The test
Create a small completed job once, put its id in SUME_JOB_ID, and run deno test --allow-net --allow-env. The first run writes the snapshot; later runs compare against it. The key is read from the environment and never written to the snapshot, because only keys and types are stored.
Deno.test("status envelope shape for a completed job", async (t) => {
const id = Deno.env.get("SUME_JOB_ID");
const key = Deno.env.get("SUME_API_KEY");
if (!id || !key) throw new Error("set SUME_JOB_ID and SUME_API_KEY");
const res = await fetch(`https://api.sume.com/v1/jobs/${id}/status`, {
headers: { Authorization: `Bearer ${key}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
const kind = (v: unknown) => (v === null ? "null" : Array.isArray(v) ? "array" : typeof v);
const shape = Object.fromEntries(
Object.entries(data).map(([k, v]) => [k, kind(v)]).sort(),
);
await t.assertSnapshot(shape);
});What Sume does and does not do
Sume returns the same envelope for async, sync and webhook jobs, with status_url, result_url, events_url and cancel_url so the client never builds paths by hand. It reports the same state twice: a queue-shaped status (IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELED) and sume_status, which never disagree.
Sume does not freeze every field forever in this snapshot; a new field showing up is the signal to read the changelog and the OpenAPI schema, then update the snapshot on purpose.
Sources
Related posts
More in Developers
- A dry-run flag for Sume API calls: print the request, skip the spend
Add DRY_RUN to code that calls the Sume API: build the body, key and spend cap, print them, and send nothing. Review a batch before it costs money.
- Dub one Short into 8 languages: Python fan-out and the total cost
Detach and transcribe once, then run one TTS job and one render per language. A Python fan-out and the per-Short bill, from Sume's catalog rates.
- eBay Media API video upload: 150 MB, MP4, and the LIVE status
eBay's Media API takes a listing video in two calls: createVideo with the exact byte size (max 150 MB), then uploadVideo as MP4. Prep the file with Sume.
- Elixir 1.20: verify a Sume webhook with :crypto.mac
An Elixir module that checks the Sume sume-v1 header with :crypto.mac and a guard clause that refuses an empty secret. Written against Elixir 1.20.4.
Written by Sume