Skip to content

Recipe: a deterministic flow test (with a host cassette) ​

Goal: lock a story's behaviour so a change can't silently regress it — without spending LLM tokens.

A flow test (Mode 2) drives the state machine with explicit intent: turns and asserts on the resulting state, world, view, and inbox. This is the test you write for almost every bug.

test_kind: flow
app: ../app.yaml
initial_state: river
initial_world:
  water_depth: 2

turns:
  - intent: { name: ford, slots: {} }
    expect_state: riverbank
    expect_world: { water_depth: 2 }

  - intent: { name: look, slots: {} }
    expect_view_matches: "You made it across"

expect_no_errors: true

When a turn drives a host or agent call that you want to be deterministic, attach a host cassette instead of real handlers. host_cassette: and host_handlers: are mutually exclusive.

test_kind: flow
app: ../app.yaml
initial_state: proposing
host_cassette: cassettes/bugfix-happy.yaml

turns:
  - intent: { name: accept, slots: {} }
    advance_clock: "200ms"          # virtual clock for async work
    expect_state: implementing
    expect_inbox: { unread: 1 }



# cassettes/bugfix-happy.yaml — the cassette file
kind: host_cassette
app_id: bugfix
episodes:
  - id: phase_2_agent
    match:
      handler: host.agent.decide
      phase: proposing
    response:
      data:
        submitted: !include artifacts/proposing.json

Before you call it fixed: confirm the test FAILS without your change. A test that passes either way isn't testing the fix. For bugs spanning concurrent I/O or rendering, assert on the combined output the user actually sees — see the tracing overview and CLAUDE.md.

Reference