# @siftline/core/testing (/docs/packages/core-testing)



The Judge takes any `SystemOneClient`. This entry ships three that never reach the network,
so a test of code that judges is fast, free and deterministic: `createScriptedClient`,
`createReplayClient` and `createRecordingClient`.

## Scripted [#scripted]

`createScriptedClient(script)` answers in call order from a list of steps, each a
`{ response }` or an `{ error }` to throw. It exposes the `calls` it received and throws
when the script runs out. The docs' own examples use it:

```ts title="scripted.ts"
import { createJudge } from "@siftline/core";
import { createScriptedClient } from "@siftline/core/testing";

import { recipe } from "./recipe";

// A test double: one recorded answer per call, in call order, never the network.
export const client = createScriptedClient([
  {
    response: {
      model: "jev-1.13.0",
      answers: {
        category: {
          type: "choice",
          choice: "complaint",
          confidence: 0.97,
          probabilities: { complaint: 0.97, question: 0.02, other: 0.01 },
        },
        wants_human: { type: "noul", noul: 0.99 },
        urgency: {
          type: "score",
          score: 1.9,
          confidence: 0.9,
          probabilities: { 0: 0.01, 1: 0.09, 2: 0.9 },
        },
      },
      usage: { input_tokens: 412, output_tokens: 60 },
    },
  },
]);

export async function judgeScripted() {
  // `now` and `id` pin the Decision's clock and id, so the output is byte-stable.
  const judge = createJudge({ client, now: () => new Date("2026-09-22T09:00:00Z") });

  return judge({ id: "msg-1", state: "Refund me now and get me a human." }, recipe, {
    id: "0192f3c2-7b1e-7c4a-9f0e-000000000001",
  });
}

```

With `now` and `id` pinned, the Decision that produces is the same bytes every run:

```json title="the Decision"
{"format":1,"id":"0192f3c2-7b1e-7c4a-9f0e-000000000001","recordId":"msg-1","recipe":{"name":"support-inbox","version":2},"model":"jev-1.13.0","judgedAt":"2026-09-22T09:00:00.000Z","trimmed":false,"answers":{"category":"complaint","wants_human":true,"urgency":2},"questions":{"category":{"confidence":0.97,"probabilities":{"complaint":0.97,"question":0.02,"other":0.01}},"wants_human":{"probability":0.99,"confidence":0.98},"urgency":{"score":1.9,"confidence":0.9,"probabilities":{"0":0.01,"1":0.09,"2":0.9}}},"confidence":0.9,"review":false,"rule":null,"action":null,"usage":{"inputTokens":412,"outputTokens":60}}
```

## Replay [#replay]

`createReplayClient(lines)` matches each request deep-equal against recorded lines and returns
the recorded answer, or throws the recorded error as an `Error` carrying its `name`,
`status`, `retryAfterMs` and `body`. A request with no match throws. The
`siftline test` output in the guide comes from a replay of real `jev-1.13.0` answers.

## Recording [#recording]

`createRecordingClient(inner, sink, options?)` wraps a real client and writes one replay line
per call to `sink`. `options.id` and `options.now` pin the line id and clock so a recording
is byte-stable. `replayLineSchema` and `parseReplayLines` are the lines' JSONL door.

Record once against the real API, commit the lines, and replay them in every test after that.
