# Offline cassettes and recording

URL: /docs/agents/cassettes
Markdown: /docs/agents/cassettes.md

> Replay HTTP and LLM exchanges with Test.fixtures, and opt into recording with --record.

Ordinary `sakura test` denies network access. `Http.recording` and `Llm.replay` let tests exercise provider-shaped exchanges offline using JSON Lines cassettes under `tests/fixtures/`.

## HTTP replay [#http-replay]

`Test.fixtures()` returns a `Dir` rooted at the package's `tests/fixtures` directory. It is read-only during ordinary tests. Install it with `Fs.rooted`, then put `Http.recording` inside an outer HTTP handler. The first handler in a `with` list is outermost.

This test belongs in the repository's `compiler/examples/secrets` package, alongside its committed `billing.jsonl` fixture:

```sakura
import std/decimal.{Decimal}
import std/fs.{Fs}
import std/http.{Http}
import std/path.{Path}
import std/request.{Request}
import std/url.{Url}

test "billing cassette replays offline" {
  with Fs.rooted(Test.fixtures()), Http.scripted([]),
      Http.recording(Path.of("billing.jsonl")) {
    let amount = Decimal.parse("12.34").unwrap_or(Decimal.zero())
    let req = Request.post_json(Url.from("https://billing/charge"), amount)
    expect_eq(Http.send(req).status, 200)
  }
}
```

A request matches on method, URL, ordinary headers and body. `Request.secret_headers` are neither recorded nor matched. Keep credentials in that secret channel; ordinary headers and bodies are cassette data. Identical requests consume matching responses in order; the last response repeats. Without recording mode, a miss fails with `HttpError` and a message pointing to `sakura test --record`.

## LLM replay [#llm-replay]

`Llm.replay(path)` installs the Anthropic adapter over `Http.recording`, an empty scripted outer HTTP handler and `Budget.unlimited()`, using a placeholder secret. It needs `Fs`, not live provider credentials. It replays generation, free text, partial callbacks and tool turns; missing exchanges fail as `ProviderError`.

The [LLM extraction example](https://github.com/icantcodefyi/sakura-lang/tree/master/compiler/examples/llm_extract) uses `Fs.rooted(Test.fixtures())`, an outer `Budget.unlimited()` for its generated `@llm` function, and `Llm.replay(Path.of("extract_ticket.jsonl"))`. The [agent-loop example](https://github.com/icantcodefyi/sakura-lang/tree/master/compiler/examples/agent_loop) also scripts its knowledge-base tool.

Each `Llm.replay` operation currently reopens the cassette. Repeated identical requests therefore replay the first matching response on each operation. For sequential responses to the same request, keep one `Http.recording` handler around an explicit `Llm.anthropic` handler instead.

From the repository root, after building the CLI:

```sh
cd compiler/examples/llm_extract
../../target/debug/sakura check
../../target/debug/sakura test
```

## Recording is opt-in [#recording-is-opt-in]

`--record` is present in `sakura test --help`. It sets recording mode, makes the fixture directory writable and grants only the manifest's network hosts. With no `net` entries it refuses to start (exit 75). Run from the prepared project directory:

```sh
sakura test --record
```

A cassette miss can then forward through the outer `Http` handler and append the response. Existing hits still replay.

For live provider recording, explicitly install `Llm.anthropic` with a real key and `Http.recording` over a host HTTP handler. `Llm.replay` keeps its empty scripted outer handler even with `--record`, so it cannot fetch a missing provider exchange itself. Review cassette diffs before committing them. The verification for this page used synthetic responses and local CLI integration tests, not a live provider.

## Synthetic cassette convention [#synthetic-cassette-convention]

The committed fixtures in `llm_extract`, `agent_loop` and `secrets` are **synthetic**, not captured live responses. Each fixture directory has a README saying that bodies, IDs and token counts were invented, naming the exchanges and describing regeneration. Their request keys were generated by executing the program with a scripted outer HTTP response under recording mode, so they match the real adapter's serialization. A changed request fails replay and requires deliberate regeneration and review.

All twelve examples now execute in run or test mode; these three replay committed fixtures. This is evidence for the example gate, not evidence that all M3 exit gates have passed.

## Still planned [#still-planned]

`--contracts` is absent from the tested CLI. Generated tests from contracts remain [planned](/docs/tour/testing#property-tests-from-contracts-planned). Streaming HTTP (`Http.stream`, `Http.read_chunk`, `Body`, `Incoming`, design §17.88) is also absent from the embedded std library; the current cassette handler implements `Http.send`.
