Libraries/cassette
cassetteTest

Record once.
Replay forever.

Your agent tests hit a paid, non-deterministic API. cassette records a whole run once — every LLM call AND every tool call — then replays it offline, deterministically, with no API key. The vcrpy of the agent era.

$ pip install cendor-cassetteRun the same test in two worlds ↓
tests/test_triage.pyoffline · deterministic · no API key
from cendor import cassette

@cassette.use("tests/triage.json")     # records once, replays forever
def test_triage():
    out = my_agent.run("My card was charged twice")
    assert cassette.semantic_match(out, "offers a refund")

# whole-run capture: LLM calls AND tool calls, in order
# replay is served through core's interceptor — no monkey-patching
How it runs

Four modes.

One decorator, four behaviors. The default, auto, records when the cassette file is missing and replays when it exists — so the first run is live and every run after is free.

Mode
What it does
auto
record if the cassette file is missing, else replay — the default
record
always run live and (re)write the cassette
replay
strict: an unrecorded call fails the test with a clear CassetteError
rerecord
run live, report drift() vs the committed cassette, never overwrite it

Matching hashes a normalized request (provider, model, messages, stream) — a pluggable normalizer decides what makes two requests "the same". Secrets are redacted on write, but matching hashes the UN-redacted request, so redaction never collapses two distinct calls. promote() turns a production JSONL trace into a replayable regression test.

Try it

Same test, two worlds.

The exact same test_triage. Flip the switch, take the network away, and run pytest — then notice which world even looks at the wifi.

▸ flip live ↔ replay · toggle the network · replay never touches the wire

Replay is served through core's interceptor seam before any network client is touched.

Semantic assertions

Meaning, not string equality.

LLM output rewords itself on every run, so exact-string asserts flake. semantic_match(actual, expected) scores meaning instead. The offline lexical default is recall-oriented — documented as negation-blind — and real semantic backends plug in: local model2vec static embeddings (the [embeddings] extra, no torch), your provider's embeddings, or an LLM judge.

semantic_drift() filters rerecord noise down to real regressions.

Honest limits

Where the edges are — by design.

The lexical default is recall-oriented.

It accepts negations and supersets — "will NOT refund" matches "refund". For adversarial checks, use the embeddings backend or an LLM judge.

Parallel workers need per-worker cassettes.

Recording is scoped to the active context; with pytest-xdist, key cassette paths per worker rather than sharing one file.

Cassette format is v2.

v1 cassettes still replay (a compatibility read path hashes the old way); new recordings include stream in the match key.

A cassette is a snapshot, not a mock.

If your prompt or tool contract changes, rerecord and review drift(); replay tests prove stability, not correctness of a new behavior.

Get started

Your agent test suite, off the meter.

$ pip install cendor-cassette

cassette docs → · pytest + xdist cookbook → · composes with the Cendor stack