Unit test Sume SDK retries with a fake fetch
createSumeClient takes a fetch option for tests. Feed it a 503 then a 200 to prove the SDK retries a GET and sends one x-api-key, with no network.

Short answer
createSumeClient accepts a fetch option that the docs describe as a seam for instrumentation, retries or tests. Pass a function that returns a 503 first and a 200 second, call any generated operation, and you can assert that the client made two attempts and sent the x-api-key header, without a server or a key that spends credits.
What the client does around your fetch
The SDK wraps whatever fetch you give it. The wrapper retries 408, 429 and 5xx responses and transport failures, up to maxRetries, which defaults to 2. A GET or HEAD is always safe to replay. A POST is replayed only when it carries an Idempotency-Key header, because replaying a run create without one would start and charge for a second run.
| Item | Value |
|---|---|
| fetch | Defaults to globalThis.fetch; a test seam |
| maxRetries | 2 |
| timeout | 10 minutes per request; 0 disables |
| Retried statuses | 408, 429, 5xx, plus transport errors |
| GET and HEAD | Always retried |
| POST | Retried only with an Idempotency-Key header |
A fake fetch test
The wrapper hands your fake a Request, so you can read its headers directly. The sample fails once and then succeeds. Because the first retry waits roughly half a second plus or minus 20 percent, the script takes about that long; keep that in mind for suites with many such cases.
Generated operations do not throw on API errors. They resolve with data, error and response, so the assertion reads error rather than catching an exception. The wait helpers are different and throw on timeout or a refused create.
import { createSumeClient, listFormats } from "@sume-com/sdk";
const keys: (string | null)[] = [];
const fake = async (req: Request) => {
keys.push(req.headers.get("x-api-key"));
if (keys.length === 1) return new Response("{}", { status: 503 });
return new Response(JSON.stringify({ data: [] }), {
status: 200,
headers: { "content-type": "application/json" },
});
};
const client = createSumeClient({ apiKey: "test-key", fetch: fake });
const { error } = await listFormats({ client });
console.log(keys.length, keys[0], error);
Cases that pay for themselves
Three more fakes cover most of the policy. First, return 400 and assert one attempt: client errors are not retried. Second, send a POST without an Idempotency-Key, return 503, and assert one attempt, which proves your code never replays an unkeyed paid request. Third, send the same POST with a key and assert that both attempts carry the identical header value.
If you wrap fetch for logging, remember the credential rule. The API rejects a request carrying both Authorization and x-api-key with 401 unauthorized, so a wrapper that adds Authorization on top of the client's own header breaks every call. A fake that records all headers makes that mistake visible in a unit test.
Limits of the seam
A fake fetch tests your wiring and the SDK's policy, not the Sume service. Keep fixtures aligned with the documented error shape, which carries error.code, error.message and error.request_id, and keep a small live smoke test on a read-only route for the real contract.
Running in CI
The whole file runs offline, so it can live in your normal unit test job without secrets. Use a throwaway string for apiKey, since the fake never contacts the service. If you also keep a live smoke test, gate it behind an environment variable and read the real key from that variable, so the offline tests pass on forks and local checkouts that have no key.
To keep timing deterministic in larger suites, you can set maxRetries to 1 for most cases and reserve the default of 2 for the one test that checks the policy. Each retry adds a real delay, because the SDK sleeps between attempts.
Sources
Related posts
More in Developers
- Cursor mcp.json ${env:NAME} for Sume's API key: no secret in the repo
Cursor's mcp.json interpolates ${env:NAME} in headers. Keep Sume's API key in an environment variable, send one credential, and know the fixed OAuth redirects.
- Cut a voiceover into sentence clips with TTS segmentation
Sume TTS returns gapless sentence segments, cutting 70 ms after each last word by default. Per-segment audio needs wav or raw; mp3 returns timings only.
- Daily spend cap in Python from a monthly Sume budget
Turn a $300 monthly budget into a $10 daily cap and enforce it with a short Python check against GET /v1/balance. Daily-cap table for $100 to $1,000.
- DBOS Python durable workflow for a Sume job: resume after a crash
Submit and poll a Sume image job in a DBOS workflow: step retries, order-derived Idempotency-Key and workflow id, tested with DBOS 3.2.0 on SQLite.
Written by Sume