Test Framework Plugins

Zero-config integration for vitest and jest. Import useAimock, write tests — the server lifecycle, env vars, and cleanup are handled automatically.

Vitest

test/app.test.ts ts
import { useAimock } from "@copilotkit/aimock/vitest";

const mock = useAimock({ fixtures: "./fixtures" });

it("responds to hello", async () => {
  // OPENAI_BASE_URL is already set
  const res = await myApp.chat("hello");
  expect(res).toBe("Hi there!");
});

Jest

test/app.test.ts ts
import { useAimock } from "@copilotkit/aimock/jest";

const mock = useAimock({ fixtures: "./fixtures" });

it("responds to hello", async () => {
  const res = await myApp.chat("hello");
  expect(res).toBe("Hi there!");
});

Options

Both plugins accept the same UseAimockOptions object:

Option Type Default Description
fixtures string — Path to fixture file or directory
patchEnv boolean true Auto-set OPENAI_BASE_URL and ANTHROPIC_BASE_URL
port number 0 (random) Port to listen on
host string 127.0.0.1 Host to bind to
logLevel string silent Log verbosity
fakesReport string "off" "off", "warn" or "fail": check each test’s MCP fake report in afterEach. "fail" fails the test, "warn" prints the report text
testIdRoot string working directory Jest only: the directory default test ids are relative to (process.cwd() when useAimock is called)

The Handle

The getter function returned by useAimock() returns an AimockFakesHandle: an AimockHandle (llm and url) plus the MCP fakes helpers fakesFor() and fakesReport(). Both types are exported from /vitest and /jest:

Property Type Description
llm LLMock The LLMock instance — add fixtures programmatically
url string The server URL (e.g., http://127.0.0.1:4010)
fakesFor() function fakesFor(testId?, { context?, mount? }) returns { testId, mcpUrl, headers }: the MCP URL and headers that reach the fakes for a test id. It also registers that test id for this test’s report. See below
fakesReport() function fakesReport(testId?, { context? }) resolves to the MCP fake report for a test id

Programmatic fixture registration

test/custom.test.ts ts
const mock = useAimock();

it("custom fixture", async () => {
  mock().llm.onMessage("custom", { content: "Custom response" });
  // ...
});

Lifecycle

Both plugins register framework hooks to manage the server automatically:

  1. beforeAll — starts the server, loads fixtures from the fixtures path, and patches OPENAI_BASE_URL / ANTHROPIC_BASE_URL environment variables
  2. beforeEach — resets match counts (sequential fixture counters return to zero, but fixtures themselves are preserved) and marks every MCP fake entry as unused
  3. afterEach — with fakesReport set to "warn" or "fail", checks the test’s MCP fake report
  4. afterAll — stops the server and restores the original environment variables

MCP fakes

The fixtures path is loaded with LLMock.loadFixtureFile() or loadFixtureDir(), so a file’s mcpFakes load too, and aimock mounts an MCPMock at each block’s mount path. A bad mcpFakes block fails beforeAll with a FixtureLoadError. Other load failures still only print a warning, as before. Because beforeEach marks the fake entries as unused, each test starts with its full list of answers.

With aimock-pytest, aimock.load_fixtures(path) posts a file’s mcpFakes to the control API with its fixtures. A file may hold only mcpFakes. When the server rejects the file with HTTP 400, the requests.HTTPError message is aimock rejected fixtures from <path>: <error>, followed by one line for each item of details. When the file has mcpFakes and the aimock server is a release without MCP fakes, load_fixtures raises RuntimeError (aimock server too old for mcpFakes: ...) and adds nothing from the file.

Fakes for a test: fakesFor()

mock().fakesFor() returns the test id, the MCP URL with ?testId=, and the matching X-Test-Id (and X-AIMock-Context) headers. Give the URL or the headers to your MCP client. mount picks the MCP mount (default /mcp). With no test id, fakesFor() uses a default made from the running test:

The default test id is used only by fakesFor() and fakesReport(). It is not added to LLM or control requests, and an explicit test id always wins. In Vitest, two tests that would get the same default (the same names in one file), a concurrent test, and a call outside a test throw AimockTestIdCollisionError; a retry of the same test reuses its id. Jest cannot tell two tests with the same full name apart, so they share one default id: pass an explicit test id for them. In test.concurrent, Jest can report another test’s name, so pass an explicit test id there too.

fakesReport is not supported in concurrent tests. The report keeps one state per useAimock(), so concurrent tests would clear and read each other’s test ids. In Vitest, with fakesReport: "warn" or "fail", a test.concurrent or describe.concurrent test fails in beforeEach with “fakesReport is not supported in concurrent tests”. Jest cannot tell the plugin that a test is concurrent, so it does not stop you: keep fakesReport: "off" for a suite with test.concurrent, or run its tests sequentially. With "off", concurrent tests can still use fakesFor() with an explicit test id.

test/weather.spec.ts ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { describe, expect, it } from "vitest";
import { useAimock } from "@copilotkit/aimock/vitest";

const mock = useAimock({ fixtures: "./fixtures", fakesReport: "fail" });

describe("weather", () => {
  it("seattle", async () => {
    // The default test id here is "test/weather.spec.ts › weather › seattle".
    const { mcpUrl } = mock().fakesFor();
    const client = new Client({ name: "weather-test", version: "1.0.0" });
    await client.connect(new StreamableHTTPClientTransport(new URL(mcpUrl)));
    const result = await client.callTool({
      name: "get_weather",
      arguments: { city: "Seattle" },
    });
    expect(result.content).toEqual([{ type: "text", text: "rain" }]);
    await client.close();
  });
});
fixtures/weather.json json
{
  "mcpFakes": {
    "scope": { "testId": "test/weather.spec.ts › weather › seattle" },
    "undeclaredTools": "deny",
    "tools": [
      {
        "name": "get_weather",
        "calls": [{ "args": { "city": "Seattle" }, "result": "rain" }]
      }
    ]
  }
}

Fake reports: fakesReport

A test can pass while its MCP fakes went wrong: many agent frameworks catch a tool error and give it to the model, and nothing fails when a declared fake is never called. With fakesReport: "fail", afterEach reads the fake report and throws AimockFakesReportError when it is not ok: a fake error (such as MCP_FAKE_MISMATCH), an unused entry, or lost state. The message lists the failures first, then the unused entry ids. "warn" prints the same text with console.warn, and "off" (the default) checks nothing.

The check covers every test id registered with fakesFor() in the test, and every test id (and context) that MCP requests to this server carried during the test, read from the journal. A test with neither is not checked. If the journal is cleared during a test, only the requests still in it are checked. mock().fakesReport() returns the report itself.

Without Plugins (Manual)

For comparison, here is the equivalent manual setup. The plugins above handle all of this for you:

test/manual.test.ts ts
import { LLMock } from "@copilotkit/aimock";

let mock: LLMock;

beforeAll(async () => {
  mock = new LLMock();
  mock.onMessage("hello", { content: "Hi!" });
  await mock.start();
  process.env.OPENAI_BASE_URL = `${mock.url}/v1`;
});

afterAll(async () => {
  await mock.stop();
  delete process.env.OPENAI_BASE_URL;
});