# Shape the world for a test

The record your test needs, the call that has to fail.

This page is executed as written by `packages/cli/src/journeys/tutorials.test.ts`; the recording
is made from the same run.

![Shape the world for a test, recorded](../media/shape-the-world-for-a-test/shape-the-world-for-a-test.gif)

There are two kinds of thing a test wants from a vendor, and Volter has one rule for each:

> If the twin stores it, create it through the vendor's own API.
> Everything else, judgment, lookups, faults, is a handler.

## Data: create it through the API

A customer, an issue, a channel, a file: anything the vendor stores, the twin stores, and the way
to put it there is the way your app would. In a test's setup, with the vendor's SDK:

```ts
const customer = await stripe.customers.create({ email: 'ada@example.com' });
```

The record is real to the twin: it has an id, it appears in lists, it can be updated and
deleted, and the twin enforces the vendor's rules about it. A record the vendor would refuse is
refused here too, which is the point. For state every test shares, put it in the seed instead;
see [seed and reset](./seed-and-reset.md).

## Behavior: write a handler

Some things a vendor does are not records. A model's answer, a search's ranking, a rate limit, a
timeout, a 500 on the third call: these are judgment and faults, and the twin has no way to know
what your test wants them to be. A **handler** tells it.

This page's app talks to Anthropic, and asks it questions with a small script:

```json file=package.json
{ "name": "agent", "private": true, "dependencies": { "@anthropic-ai/sdk": "^0.30" } }
```

```text file=.env.example
ANTHROPIC_API_KEY=
```

```js file=ask.mjs
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, maxRetries: 0 });
try {
  const message = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 64, messages: [{ role: 'user', content: process.argv[2] }] });
  console.log(200, message.content.find((c) => c.type === 'text')?.text ?? '');
} catch (error) {
  console.log(error.status, error.error?.error?.type ?? error.message);
}
```

```bash
npm install
npm install -g @volter/world
npm install -D @volter/twin-anthropic
volter world init
```

Handlers live in `.volter/handlers/<vendor>.json`, one file per twin; `init` copied the
anthropic twin's starter there. The grammar is small: match a request, respond, or inject a fault.

```json file=.volter/handlers/anthropic.json
{
  "handlers": [
    { "id": "triage-answer", "on": { "userTextIncludes": "classify this ticket" }, "respond": { "text": "billing" } },
    { "id": "flaky-once", "on": { "userTextIncludes": "summarize" }, "once": true, "fault": { "kind": "status", "status": 529, "message": "Overloaded" } }
  ]
}
```

- `on` is what the handler matches: a phrase in the user's text, a tool the caller offered, a
  path, a query. Each twin documents its matchers at `GET <twin-url>/twin`.
- `respond` is what the twin answers: text, or a tool call, in the vendor's own shape.
- `fault` instead of `respond` injects a failure: `{ "kind": "status", "status": 529 }` answers
  the vendor's own error envelope with that status, `{ "kind": "slow", "ms": 3000 }` delays,
  `{ "kind": "drop" }` holds the socket.
- `once` fires the handler one time and then lets the twin's default behavior through, which is
  how a test gets "fails, then succeeds".

Handlers are data, not code, so they are deterministic, diffable and committed with the world.
A twin reads them when it starts:

```bash
volter world up --no-seed
```

```text
acme-web  1 twin up
```

The scripted answer, where the handler matches:

```bash
volter world run -- node ask.mjs "classify this ticket: my card was charged twice"
```

```text
200 billing
```

The fault, once, then the default behavior, which for a generative twin is a labeled
deterministic stub. The script sets `maxRetries: 0` to show the fault; with the SDK's default
retries on, it would retry the 529 and print the stub on the second try, exactly as production
code would, which is what a once-fault is for: it exercises the retry path.

```bash
volter world run -- node ask.mjs "summarize the thread"
```

```text
529 overloaded_error
```

```bash
volter world run -- node ask.mjs "summarize the thread"
```

```text
200
```

## See what matched

```bash
volter world run -- sh -c 'curl -s "$ANTHROPIC_TWIN_URL/twin/scenario"'
```

```text
"id":"triage-answer","matches":1
"id":"flaky-once","once":true,"matches":1
```

A miss is a request no handler matched, and the recent ones carry enough of the request to write
the handler you were missing. Unmatched asks are also in the world's log, so `volter world log`
shows them in order with everything else the app did.

```bash
volter world down
```

## Time

A test that needs "thirty days later" sets the world's clock rather than waiting or mocking
`Date`. Every twin stamps records from it: `volter world clock advance 30d`. See
[seed and reset](./seed-and-reset.md).

## Faults that are not handlers

Some faults are the twin's own surface, because the vendor's are. A twin in `--read-only` mode
refuses every write with the vendor's own refusal shape, which exercises your error paths. A
vendor with rate limits enforces them as the vendor documents them. Each twin's README says which
of these it models.

<!-- playback:BEGIN — GENERATED by `bun scripts/docs-media.ts`; do not edit between markers -->

## Playback

Each command above, as the recording shows it.

<details><summary><code>npm install</code></summary>

![step 1](../media/shape-the-world-for-a-test/step-01.png)

</details>
<details><summary><code>npm install -g @volter/world</code></summary>

![step 2](../media/shape-the-world-for-a-test/step-02.png)

</details>
<details><summary><code>npm install -D @volter/twin-anthropic</code></summary>

![step 3](../media/shape-the-world-for-a-test/step-03.png)

</details>
<details><summary><code>volter world init</code></summary>

![step 4](../media/shape-the-world-for-a-test/step-04.png)

</details>
<details><summary><code>volter world up --no-seed</code></summary>

![step 5](../media/shape-the-world-for-a-test/step-05.png)

</details>
<details><summary><code>volter world run -- node ask.mjs "classify this ticket: my card was charged twice"</code></summary>

![step 6](../media/shape-the-world-for-a-test/step-06.png)

</details>
<details><summary><code>volter world run -- node ask.mjs "summarize the thread"</code></summary>

![step 7](../media/shape-the-world-for-a-test/step-07.png)

</details>
<details><summary><code>volter world run -- node ask.mjs "summarize the thread"</code></summary>

![step 8](../media/shape-the-world-for-a-test/step-08.png)

</details>
<details><summary><code>volter world run -- sh -c 'curl -s "$ANTHROPIC_TWIN_URL/twin/scenario"'</code></summary>

![step 9](../media/shape-the-world-for-a-test/step-09.png)

</details>
<details><summary><code>volter world down</code></summary>

![step 10](../media/shape-the-world-for-a-test/step-10.png)

</details>

<!-- playback:END -->
