@siftline/core/testing
Three clients that judge without the network.
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
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:
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:
{"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
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
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.