Skip to main content
Testing agents correctly means running them without real model API calls, in a way that’s fast and deterministic. Vibes provides two main options: TestModel auto-generates plausible tool calls and a final response on every run; FunctionModel gives you full per-turn control so you can simulate exact model behavior. Both integrate with agent.override({ model }) to drop in a test model without changing your agent definition.

Preventing accidental API calls

Call setAllowModelRequests(false) once at the top of your test file to raise an error if any agent accidentally reaches the real model API. This guards against tests that forget to use a test model.
import { setAllowModelRequests } from "@vibesjs/sdk/testing";

// At the top of your test file - prevents any real model call
setAllowModelRequests(false);
setAllowModelRequests(false) is module-level global state. Only call it once at the top of a test file setup block. agent.override({ model }) bypasses this guard automatically - test models always work even when real calls are blocked.

TestModel

TestModel is the simplest option. It auto-generates schema-valid tool calls for any tools the agent has, then returns a final result.
import { setAllowModelRequests, TestModel, createTestModel } from "@vibesjs/sdk/testing";
import { z } from "zod";

setAllowModelRequests(false);

// Basic usage: auto-generates tool calls, then returns "test response"
const model = new TestModel();
const result = await agent.override({ model }).run("Hello");
// result.output === "test response"

// With typed output schema: auto-generates schema-valid output
const OutputSchema = z.object({ answer: z.string() });
const typedModel = createTestModel({ outputSchema: OutputSchema });
const typedResult = await agent.override({ model: typedModel }).run("Hello");
// typedResult.output.answer === "test response" (schema-valid string)
TestModel accepts an optional options object:
OptionTypeDefaultDescription
callToolsbooleantrueAuto-generate tool calls on the first turn before returning final result
textstring"test response"Text returned when there is no output schema
outputSchemaZod schema-When provided, generates schema-valid structured output
createTestModel({ outputSchema }) is a convenience factory equivalent to new TestModel({ outputSchema }).

FunctionModel - per-turn control

When you need precise control over what the model returns on each turn, use FunctionModel. Its constructor takes a function that receives { messages, tools, turn } and returns a ModelResponse.
import { FunctionModel } from "@vibesjs/sdk/testing";

const model = new FunctionModel(({ messages, tools, turn }) => {
  if (turn === 0) {
    // First turn: return a tool call
    return {
      content: [
        {
          type: "tool-call",
          toolCallId: "tc-1",
          toolName: "search",
          input: JSON.stringify({ q: "hello" }),
        },
      ],
      finishReason: { unified: "tool-calls", raw: undefined },
      usage: {
        inputTokens: { total: 1, noCache: 1, cacheRead: 0 },
        outputTokens: { total: 1 },
      },
      warnings: [],
    };
  }
  // Second turn: return text
  return {
    content: [{ type: "text", text: "The answer is 42" }],
    finishReason: { unified: "stop", raw: undefined },
    usage: {
      inputTokens: { total: 1, noCache: 1, cacheRead: 0 },
      outputTokens: { total: 1 },
    },
    warnings: [],
  };
});

const result = await agent.override({ model }).run("What is the answer?");
The function receives the current messages array and tools definition on every turn, so you can assert on what the agent sent before deciding what to return.

captureRunMessages

captureRunMessages wraps an agent run and captures the ModelMessage[] arrays that were passed to the model on each turn, letting you assert on exact message content:
import { captureRunMessages, TestModel } from "@vibesjs/sdk/testing";

const model = new TestModel();

const { result, messages } = await captureRunMessages(() =>
  agent.override({ model }).run("Hello")
);

// messages[0] = ModelMessage[] sent to model on turn 0
// messages[1] = ModelMessage[] sent to model on turn 1 (if multi-turn)
console.log(messages[0]); // inspect system prompt, user message
console.log(result.output);
captureRunMessages is not safe for concurrent agent runs - it uses module-level state that is reset after each call. Use it for sequential assertions only. Do not run multiple agents in parallel inside the same captureRunMessages callback.

agent.override()

agent.override({ model }) returns a runner object - it is not an Agent instance. The runner exposes the same three execution methods:
const runner = agent.override({ model });

// All three run methods are available:
const result = await runner.run("Hello");
const stream = runner.stream("Hello");
for await (const event of runner.runStreamEvents("Hello")) { ... }
agent.override() returns { run, stream, runStreamEvents } - not an Agent. You cannot pass the override result where an Agent is expected, and it cannot be stored in a variable typed as Agent.
The override call bypasses the setAllowModelRequests(false) guard automatically, so your test models always work even when real API calls are blocked.

API reference

SymbolSignatureDescription
setAllowModelRequests(allow: boolean) => voidModule-level guard - throws if a real model call is attempted when false
TestModelnew TestModel(options?)Auto-generates tool calls and final result; use for most agent tests
createTestModel({ outputSchema }) => TestModelConvenience factory for typed output tests
FunctionModelnew FunctionModel(fn)Per-turn model control; fn receives { messages, tools, turn }
captureRunMessages(fn) => Promise<{ result, messages }>Captures ModelMessage[][] for all turns; sequential only
agent.override(options) => { run, stream, runStreamEvents }Swap model for a test; bypasses setAllowModelRequests guard