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.

5 min readSume
All posts

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.

Deno 2.9 snapshot facts (read 2026-10-03)
ItemDetail
t.assertSnapshot()available on the test context without an import
Storage__snapshots__/<test file>.snap beside the test
Updatedeno 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.

Status fields that change type with state (as of 2026-10-03)
FieldRunningTerminal
terminalfalsetrue
result_readyfalsetrue when completed
next_poll_after_secondsnumbernull
cancel_urlstringnull

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

All Developers posts

Written by Sume