Recorded-session snapshots

The recorded-session snapshot suite checks Oda against committed evidence instead of a live model. Each scenario replays a recorded fixture: it starts the real agent as a subprocess, drives it with a scripted input, and compares the protocol transcript, the session log and the files the run wrote against what was committed. It is developer tooling for people who add or maintain these scenarios. To choose between the test tiers, see Testing tiers.

Run, record and refresh

The suite runs from your checkout of the Oda source, in three modes:

sh
pnpm run test:snapshot
pnpm run test:snapshot:record
pnpm run test:snapshot:refresh

What a scenario holds

FileWhat it holds
snapshot.ymlThe scenario's manifest.
input.jsonThe deterministic script the run is driven with.
session.jsonl, session.<n>[.vN].jsonlCommitted session fixtures: the parent session and its children, one file per generation.
stdout.expected.jsonlThe normalised ACP transcript the run must match, one compact JSON frame per line.
stdout.expected.windows.jsonlThe same transcript with native path separators, for scenarios that pin Windows output.
system-prompt.expected.md, system-prompt.<n>.expected.mdThe full system prompt text behind the {{system}} tokens, per role.
tool-schemas.expected.json, tool-schemas.<n>.expected.jsonThe full tool schemas behind the {{tools}} tokens, per role.
workspace/, workspace.expected/Files seeded into the run folder before it starts, and the expected end state when the scenario declares a final snapshot.
replay.override.jsonPresent exactly when a scenario needs scripted throws or hangs that the recorded stream cannot express; the shapes are on Session replay fixtures.

Session generations in fixtures

Committed fixtures keep their generations side by side, under these shapes and rules:

Tokens keep fixtures portable

A committed fixture replaces everything machine-specific with typed tokens.

The scenario manifest

snapshot.yml is a closed schema: unknown fields are refused, no JavaScript YAML tags are allowed, and names are lower-kebab-case. A minimal manifest is:

yaml
version: 1
profile: acp

Input operations and permission asks

When the suite refuses

Questions, answered

What is the difference between record and refresh?

Record calls the live model: it re-records a live scenario and writes the new session generation and the sidecars. Refresh needs no key: it replays the committed scripts and rewrites the derived files. Authored scenarios are never re-recorded, so for them refresh is the only mode that writes.

Do I need a model key to run the suite?

No. Replay and refresh are keyless; only recording calls the live model.

Which generation of a fixture does the suite compare?

Each role, the parent session and each child, is compared at its own highest generation. Older generations stay in the folder beside it and are not compared, which is also why record and refresh never rename or delete one. The format's own versioning rules are on Chat log format and compatibility.

Can I edit a committed fixture by hand?

The committed set is guarded, so an edit must pass the storage checks: the file must be a redaction fixed point and must not carry an unscrubbed system prompt or tool schemas, a request header with no preceding system message, a non-canonical workspace token, or a placeholder tool name. Regenerating is usually easier: refresh rewrites the derived files, and record re-records a live scenario.

Where is the full system prompt when the fixture only shows {{system}}?

In the sidecar file next to it: system-prompt.expected.md, or a numbered sidecar for a child role, with the tool schemas in tool-schemas.expected.json. The fixture keeps the token to mark presence; the comparison restores the full text from the sidecar. A changed prompt is appended under a <!-- system/message change N --> marker.

Why did the suite refuse my scenario at registration?

The scenario table is checked before anything runs, and every header class must have exactly one scenario that pins its request-header content. With none, or with two, the suite refuses and names the class. A duplicate scenario name is refused too, and pin-only fields are refused on a scenario that does not pin.

When is a scenario skipped?

Recording skips authored scenarios and retained historical generations; both still run in replay and refresh. A scenario that declares one platform runs only there, while the fixture guards run everywhere.

What happens to a permission request my scenario does not script?

It is answered cancelled. Scripted answers are consumed in order and matched by option kind, so a request beyond the queue, or with no queue, is cancelled; a scripted kind the request does not offer fails the run.

Why did a wait fail with "did not persist turn/end"?

The wait polled for that event in the stored log and its budget ran out, ten seconds by default. Check that the scripted input produces the event; the message names the event it waited for and the budget that expired.

How does the suite check the files a run wrote?

Through workspace snapshots. A scenario can copy a committed workspace/ folder into the run folder before it starts, and declare a final snapshot: the run's end state is then compared entry by entry as text, binary, symlink or empty directory. A run that changes the workspace without a declared final snapshot is warned that a changed workspace requires workspace.final.