Sakura Lang

Offline cassettes and recording

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

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:

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(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 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 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:

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

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:

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

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

--contracts is absent from the tested CLI. Generated tests from contracts remain 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.

Edit on GitHub

On this page