Help us shape the future of Go! Take the 2026 Go Developer Survey

cassette

command module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 21 Imported by: 0

README ΒΆ

cassette πŸ“Ό

Record your AI calls once. Replay them forever.

Fast, free, and identical every time, so your tests stop waiting on the AI.

tests Made with Go


The problem

Your app talks to an AI. Your tests do too. And that hurts in three ways:

  • 🐒 It's slow. Every AI reply takes a few seconds, so your tests take minutes.
  • πŸ’Έ It costs money. You pay for every call, on every run, forever.
  • 🎲 It's random. The AI words things a little differently each time, so a test passes on Monday and fails on Tuesday, even though you changed nothing.

The fix

cassette is a tape recorder that sits between your app and the AI.

            first run:  your app  ──▢  πŸ“Ό cassette  ──▢  the AI
                                         β”‚
                                         └──▢ saves the answer to a file

  every run after that:  your app  ──▢  πŸ“Ό cassette     (the AI is never called)
                                         β”‚
                                         └──▢ plays back the saved answer

The first time, cassette passes your question to the real AI and saves the answer. After that, it plays the saved answer back instantly: no waiting, no bill, no surprises.

In one of our tests against Google's Gemini:

Live AI Replayed by cassette
Normal reply 3.6 seconds 0.02 seconds
Streamed reply 3.1 seconds 0.008 seconds
Cost a few tokens $0

Quick start

1. Install it. Download the program for your computer from the latest release, or, if you have Go:

go install github.com/TemirlanN-dev/cassette@latest

2. Add two scripts to your package.json:

"scripts": {
  "test": "cassette replay -- jest",
  "test:record": "cassette auto --target <your AI service> -- jest"
}

Swap jest for whatever runs your tests, and <your AI service> for its address:

AI service Address
Anthropic (Claude) https://api.anthropic.com
OpenAI (ChatGPT) https://api.openai.com
Google (Gemini) https://generativelanguage.googleapis.com
Mistral https://api.mistral.ai
Anything else the address its docs tell you to send requests to

Not using Anthropic or OpenAI? Add one --env option too. It takes a minute: see How does my app find cassette?

3. Record once, with your API key set as usual:

npm run test:record

4. Then just test like you always do. No API key, no cost, no waiting:

npm test

That's it. Your recordings live in cassettes/default.jsonl. Commit that file, and everyone on your team (and your CI) gets fast, free tests too.

Not using npm? The same idea works with any test command, like cassette replay -- pytest or cassette replay -- go test ./....

How does my app find cassette?

Out of the box, your app doesn't know cassette exists. It sends requests straight to the AI, because that's what its AI library is set up to do:

Normally:            your app  ─────────────────────────▢  the AI
                                 (cassette is running, but nobody is talking to it)

So something has to tell your app: "send your requests to cassette instead."

Think of the AI's address as a contact saved in your phone. Your app "calls" that contact whenever it needs the AI. To put cassette in the middle, you change the number saved in that contact to cassette's. From then on, every call goes to cassette, and cassette passes it on:

While testing:       your app  ──▢  πŸ“Ό cassette  ──▢  the AI

In AI libraries, that saved contact is a setting called base URL. The good news: cassette changes it for you.

Using Anthropic or OpenAI? Nothing to do.

cassette points their libraries at itself automatically. The Quick start above is all you need.

Using anything else? Add one option.

Most AI libraries can also read their base URL from an environment variable. Look in your library's docs for its name (something like SOME_AI_BASE_URL), then tell cassette about it with --env:

"scripts": {
  "test": "cassette replay --env SOME_AI_BASE_URL=/v1 -- <your test command>",
  "test:record": "cassette auto --target https://api.some-ai.com --env SOME_AI_BASE_URL=/v1 -- <your test command>"
}

Both values come straight from your AI's normal address, split in two:

https://api.some-ai.com    +    /v1
       --target                 the path for --env

That's it: no code changes. Prefer shorter scripts? Put the --env part in cassette.yaml instead:

env:
  SOME_AI_BASE_URL: /v1
My library has no such environment variable

Then set its base URL yourself in your test setup (your real app doesn't change), using cassette's address with the same path:

// tests/setup.js
const client = new SomeAIClient({ baseURL: "http://127.0.0.1:4000/v1" });

And add --port 4000 to both scripts, so cassette's address stays the same every run.

Three ways to play the tape

Command What it does Use it when
πŸ” cassette auto Plays back what it knows, records what it doesn't You're writing or changing tests
▢️ cassette replay Only plays back. Anything new is an error In CI, so nobody gets a surprise bill
πŸ”΄ cassette record Throws away the old tape and records everything fresh Your prompts changed a lot and you want a clean start

In replay, a request that was never recorded fails loudly with a clear message telling you to run cassette auto. It even fails the run if your app quietly caught the error and your tests passed anyway.

Smart about things that change

Some apps put things in their requests that are different every run, like today's date or a random ID:

Monday:   "Today is 2026-10-19. Summarize my emails."
Tuesday:  "Today is 2026-10-20. Summarize my emails."

A plain recorder would think those are two different questions and fail. cassette knows dates, times and random IDs aren't really part of the question, so both match the same recording. It works out of the box.

Got something else that changes? Add a cassette.yaml next to your tests:

# Ignore these fields completely
ignore:
  - metadata.user_id
  - messages.*.id          # the "id" in every message

# Swap changing text for a fixed placeholder before comparing
replace:
  - pattern: 'C:\\Users\\[^\\]+'
    with: '<home folder>'

# Turn off the built-in date / time / ID rules
# default_rules: false

Your recordings always keep the original text; these rules only affect how requests are compared.

Streaming works too

When an AI "types" its answer a few words at a time, cassette records every piece and plays it back the same way, so your app behaves exactly like it does with the real thing.

Replay is instant by default. Want the original typing speed (say, to test a loading animation)?

cassette replay --realtime -- npm test

Safe to commit

Recordings are made to live in git, so cassette never saves your API keys (or your account IDs). They're removed before anything is written.

See what changed

Edited a prompt and re-recorded? Ask cassette what's different:

cassette diff
~ POST /v1/messages   request changed (probably the same call with an edited prompt)
    request:
      messages.0.content: "Find cat facts" β†’ "Tell me a cat fact"
    answer:      "Let me search."
             β†’   "Cats sleep 13 hours a day."
    tool calls:  search_web({"q":"cat facts"})
             β†’   (none)

1 changed, 0 added, 0 removed, 1 unchanged

It compares your recordings with your last commit and understands answers and tool calls from Anthropic, OpenAI and Gemini. Things that change on every recording, like message IDs, don't count as changes. Want to compare any two files instead? cassette diff old.jsonl new.jsonl.

Readable diffs in Git, too. Cassette files store one recording per line, which keeps saving fast and simple, but makes raw diffs hard to read. Tell Git to show them through cassette, once per repo:

git config diff.cassette.textconv "cassette show"

And add this line to your .gitattributes file:

cassettes/*.jsonl diff=cassette

Now git diff and VS Code show the requests, answers and tool calls instead of long lines of JSON. (GitHub's website still shows the raw lines, so run cassette diff when reviewing a pull request.)

Use it in CI

Because replay never calls the AI, your CI doesn't need an API key at all. Here's a GitHub Actions example:

- uses: actions/setup-go@v7
  with:
    go-version: stable
- run: go install github.com/TemirlanN-dev/cassette@latest
- run: npm test    # runs "cassette replay -- jest" from your package.json

If a test sends a request that isn't recorded, the build fails and tells you which one.

All the options

cassette <record | auto | replay> [options] [-- your test command]
cassette diff [OLD NEW]     show what changed in your recordings
cassette show FILE          print a cassette in readable form

  --target URL     the real AI service to record from (needed for record and auto)
  --cassette FILE  where recordings are saved (default: cassettes/default.jsonl)
  --config FILE    your settings file (default: cassette.yaml, optional)
  --env NAME=PATH  also point this environment variable at cassette (repeatable)
  --port NUMBER    which port to listen on (default: a free one when running a command, else 4000)
  --realtime       replay streamed answers at their original speed (auto and replay)

No test command? cassette runs on its own until you press Ctrl+C, and you point your app at it yourself.

Good to know

cassette works with any AI service that answers ordinary web requests. A few things it doesn't handle (yet):

  • Audio and image responses. It's built for text and JSON answers.
  • Live voice / "realtime" APIs. These keep a two-way connection open instead of asking and answering.
  • One tape per test. For now, a whole test run shares one recording file.

Coming soon

  • πŸ§ͺ Jest, Vitest and pytest helpers: cassette starts by itself when your tests do, with a separate recording for each test
  • πŸ’° Cost report: how many tokens and dollars each test would have cost

Contributing

Ideas, bug reports and pull requests are all welcome! See CONTRIBUTING.md to get started.

License

MIT. Do whatever you like with it. Press record. πŸ“Ό

Documentation ΒΆ

The Go Gopher

There is no documentation for this package.

Directories ΒΆ

Path Synopsis
internal
cassette
Package cassette defines the recording file format and how it is saved.
Package cassette defines the recording file format and how it is saved.
config
Package config reads cassette.yaml.
Package config reads cassette.yaml.
inspect
Package inspect reads a recorded response and pulls out what a person cares about: the AI's answer and the tools it called.
Package inspect reads a recorded response and pulls out what a person cares about: the AI's answer and the tools it called.
match
Package match decides whether two requests count as "the same" by turning each one into a fingerprint.
Package match decides whether two requests count as "the same" by turning each one into a fingerprint.
proxy
Package proxy is the HTTP server that sits between the app and the real API.
Package proxy is the HTTP server that sits between the app and the real API.
report
Package report turns cassettes into text for people: `cassette show` prints one cassette readably, and `cassette diff` explains what changed between two.
Package report turns cassettes into text for people: `cassette show` prints one cassette readably, and `cassette diff` explains what changed between two.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL