Skip to content

CI-safe cassettes

StableSince v0.1.0

A cassette makes a network-dependent test deterministic, but only if the recording is a reviewed artifact. The rule this recipe encodes:

CI replays. Only a human records.

The package enforces the first half: when CI is set, auto mode refuses to record, a missing cassette fails the test, and snapshots run in ci mode and never write a baseline. The second half is a workflow decision, and it is the one that keeps secrets and flaky live responses out of your history.

The modes, and which one CI gets

VCR_MODERecords?In CI (CI set)
auto (default)Only when the cassette is missingRefuses. A missing cassette fails the test instead of recording
recordAlways, overwriting at the end of the runStill records if you set it explicitly — do not set it in CI
replayNever; a missing cassette failsSame, and the explicit form if you want CI to be loud about it
passthroughNever; ignores the cassetteSame, and it means “go to the network” — never in CI

The default needs no configuration: pipelines that set CI (GitHub Actions does) get refusal-to-record for free. Setting VCR_MODE=replay in the pipeline additionally documents the intent.

Where the files live

ArtifactPathCommit it?
HTTP recording__cassettes__/<test-slug>.json, next to the test fileYes
Callback sidecar__cassettes__/<test-slug>.callbacks.jsonYes
Snapshot baseline__snapshots__/<test-slug>.snap.jsonYes

The conventions are fixed to the test file’s directory in 0.1.x: there is no base-directory override. A recording’s name follows the test name, so renaming a test orphans its cassette — clear the old file when you rename.

The recording workflow

  1. Write the test with the network in place. Run it once locally with the default auto mode; the first run records.

    Terminal window
    bun test tests/user.test.ts
  2. Review what was recorded, before anything else:

    Terminal window
    git status --short
    git diff -- __cassettes__

    Read the request URLs, status codes, and bodies. Look for tokens, cookies, personal data, and anything that would date badly.

  3. Re-record deliberately when the API changes. Do not let auto refresh a request for you: a missing request fails with CASSETTE_MISMATCH by design, and the message names the clear command.

    Terminal window
    bunx test-utils cache clear --file tests/user.test.ts --test "fetches user details"
    bun test tests/user.test.ts
    git diff -- __cassettes__
  4. Commit the cassette with the code change that caused it, so the diff is reviewed as part of that change.

  5. Let CI replay. No pipeline change is needed beyond committing the files.

Secrets

Redaction covers sensitive request headers only, and configurable redaction is not part of 0.1.x. So:

  • Never record against production credentials. Point the test at a staging environment or a local server, and pass throwaway values.
  • Replace credentials before the request, not after: a token that reaches the recorder is in the cassette until you clear and re-record it.
  • Treat every new cassette as sensitive until you have read it. Committed recordings contain real response bodies.
  • Rotate anything that was recorded by mistake. Clearing the cassette removes it from the working tree, not from git history.

What CI should not do

Anti-patternWhy it breaks
VCR_MODE=record bun test in a pipeline jobEvery run rewrites recordings; the diff is never reviewed, and a live response can be captured with real data
Running with CI unset to “make it pass”That is the failure mode the guard exists to prevent — record locally instead
SNAPSHOT_MODE=update in CIBaselines accepted by a machine are never reviewed by a human
Committing __cassettes__/ in .gitignoreCI then has nothing to replay, and the suite becomes network-dependent in the worst place
Recording against testServer URLsThe port changes every run, so the recording can never replay; use httpMock for local endpoints

Failure modes in CI

SymptomMeaningAction
CASSETTE_NOT_FOUNDThe test has no committed recording, or the file was clearedRecord locally, review, commit
CASSETTE_MISMATCHThe request changed (method or full URL) since the recordingClear the test’s recording locally, re-record, review the diff, commit
CALLBACK_NOT_RECORDEDThe callback body changedRe-record the callback; cassette(fn) refreshes a changed body in local auto mode with a warning
A snapshot test fails with a diffThe baseline is staleRead the diff; if the change is intended, clear and re-record that one snapshot locally
Everything passes locally, CI fails with differencesReal time, randomness, or the network leaked into the testAdd clock, seed, and networkGuard — see Deterministic tests

Docs for bun-test-utils v0.1.0 Release notes Report a docs issue