Code suites: TypeScript or JavaScript
JSON is great for data. When you want to call your agent directly, or score with your own logic, write the suite in code:
behavtest init --ts # writes behavtest/suite.mts: no server, no API key
behavtest run behavtest/suite.mts
// support-bot.suite.ts
import type { CodeSuite } from "behavtest";
import { answer } from "./agent.ts"; // your real agent; write the .ts extension in local imports
export default {
name: "support-bot",
defaults: { repeat: 3 },
// Test a function in your own process. (Or keep { adapter: "http", config: { url } }.)
pipeline: {
name: "support-agent",
config: { model: "claude-sonnet-5", promptVersion: "v7" }, // recorded with each run (secrets are masked)
run: async (input) => {
const r = await answer(String(input));
return { output: r.text, costUsd: r.costUsd, usage: r.usage }; // or just return a string
},
},
scorers: {
// A custom scorer is just a function. Return a boolean, or { pass, value, reasoning }.
citesPolicy: ({ output }) => /policy #\d+/i.test(output),
underBudget: ({ meta }) => ({ pass: (meta.costUsd ?? 0) < 0.01, value: meta.costUsd, reasoning: `$${meta.costUsd}` }),
},
cases: [
{ id: "refund-window", input: "How long do I have to return an item?", scorers: ["citesPolicy", "underBudget"] },
],
} satisfies CodeSuite;
- What is allowed: everything a JSON suite has, plus
scorers(name → function) and apipelinewith arunfunction. Built-in scorers sit alongside yours. A scorer may also be an object{ score, requiresExpected?, preflight?, fingerprint? }.export defaultmay be an (async) function that returns the suite. - TypeScript without tooling: Node imports
.ts/.mtsfiles natively by stripping types: no loader, no build step, no extra dependency. That means type syntax only (noenum,namespaceor parameter properties), and local imports must include the.tsextension.import type { CodeSuite } from "behavtest"is erased, sonpx behavtestworks without installing anything in your project (a value import such asdefineSuiteortracerneedsnpm i -D behavtest). Prefer plain JavaScript? A.mjssuite has the same shape. - File extension and module type: suites are ES modules. A plain
.ts(or.js) file is treated as an ES module only if yourpackage.jsonsays"type": "module";npm initwrites"type": "commonjs", in which case use.mts/.mjs(whatbehavtest init --tsdoes, so it works in any project), and give local helper files the same treatment. BehavTest tells you when this is the problem. - Timeouts are enforced for you. Every attempt and every scorer is bounded by
--timeout(default 30 s), even if your code ignores theAbortSignalit is given; a hung function becomes an errored attempt, not a hung run. - Editing a scorer is a change, not a regression. Each inline scorer is fingerprinted from its source, and the fingerprint is part of its cases' identity, so after you edit one,
behavtest comparereports those cases asmodifiedinstead of comparing results produced by different logic. (Changes in code the scorer imports are not detected: bumpfingerprintif you keep logic in a helper.) - Suite files run code. Loading a code suite executes it, exactly like a test file: only run suites you trust. JSON suites are pure data.