tarek.kharsa

home / lessons / opencode · lesson 3

TestingOpenCode · lesson 3 of 82 min read

Record real HTTP once, replay it forever

Cassettes instead of mocks, and a CI that refuses to record.

Problem

Hand-written HTTP mocks return what you think the API sends, not what it really sends.

Idea

Record real traffic once and replay it forever. Fail in CI when a recording is missing, and redact secrets before saving.

Hand-written HTTP mocks lie in a quiet way: they return the response you think the provider sends. Streaming chunk boundaries, retry headers, odd status codes and WebSocket frame order get simplified away, and those are exactly where provider bugs live. The agent guide's first testing rule is "Avoid mocks as much as possible."

So the repo built a recorder, and published it as its own package.

Record once, replay after

it.effect("loads a user", () =>
  getUser.pipe(Effect.provide(HttpRecorder.http("users/get-one"))),
)
// first local run: calls the API, writes recordings/users/get-one.json
// later runs:      replays the cassette, no network
  1. Cassette recorded?Replay it. Application code can't tell whether a response is live or replayed.
  2. Missing, running locally?Call the real service once and save the exchange as a reviewable JSON file.
  3. Missing, on CI?Fail. When CI=true a missing cassette is an error, never a live call, so tests can't quietly start hitting the network.

The details that keep it honest

No overwrite mode

To refresh a recording you delete its file and rerun. "Deletion makes the set of recordings being refreshed visible and reviewable."

Redacted before disk

Most headers are dropped, and credentials in headers, URLs and JSON bodies are redacted by default. You extend it for your own fields, like an account ID in a path.

WebSockets in order

Replay releases server frames until the next recorded client frame, then waits for the app to send a matching one. Chronology is part of the test.

Binary-safe

Binary frames are stored losslessly as base64, and client and server frame kinds must match on replay.

Where it's used

Recorded tests cover real provider streams in the core, LLM and main application packages, the place where mocks would be most misleading: a chat stream's exact chunking, a provider's error shape, a WebSocket transport's message order.

Steal this

Replace hand-written HTTP mocks with recorded cassettes. Fail in CI when one is missing, make refreshing a deliberate delete-and-rerun, and redact secrets before they're written, not after.

Use it in your code

A prompt for your coding agent

Run it from the root of your repo. It checks whether the idea fits before it changes anything.

Replace hand-written HTTP mocks in this project's tests with recorded cassettes.

The idea: record real HTTP traffic once, save it as a reviewable file, and replay it in later runs. In CI, a missing recording fails instead of calling the network. To refresh a recording you delete it and rerun the test, and secrets are redacted before anything is written.

1. Find tests that mock HTTP clients or external APIs by hand, and note which would benefit most from real responses: streaming, retries, pagination, error shapes.
2. Check whether the stack already has a record-and-replay tool (for example Polly.js, nock's recorder, VCR.py or go-vcr) and prefer it.
3. Set it up for one test: record only when the cassette is missing and not in CI, replay otherwise, fail on a missing cassette when CI=true, and redact auth headers, tokens and personal data.
4. Commit the cassette and show me its redacted content.

Never record against production with real customer data. Keep the change small.

Read the source

Where this comes from

OpenCode (anomalyco/opencode), the open-source coding agent (these lessons are from its upcoming v2). The files behind this lesson: