Skip to main content

The Testing Module

claude-code/testing is the test kit that *.test.ts and *.test.tsx files import. claude plugin test runs those files with no session, no sign-in, and no network. A test runs in an environment like that of a hook, with no files, no network, and no process. A hooks module in a session cannot import this module.

Each test ends after 5 seconds unless it sets timeoutMs. Test files live in the plugin folder.

Official page: Test a Mod, Mods Reference. Source: the Mod API type file of the 2.1.312 program (schemas/claude-code.d.ts).

Example​

This test fires two tool calls and checks that /tally counts both.

tally.test.ts
import { expect, test } from "claude-code/testing";

test("/tally reports the tool calls the mod has seen", async ($, on) => {
on("tool.call", () => ({ result: "ok" }));
await $.tool.call({ tool: "Bash", command: "ls" });
await $.tool.call({ tool: "Read", file_path: "README.md" });
const answer = await $.command.run({ command: "tally", args: "" });
expect(answer.text).toBe(
"Claude has made 2 tool calls since this mod loaded",
);
});

Exports​

ItemStatusWhat it doesHow it worksWhere it can be used
test(name, body)
Documented: An official docs page names this item.
Declares one test. body is async ($, on) => {}.$ is the engine's own, loaded from the plugin folder. The hooks that on registers sit beneath every plugin. Default timeout 5 seconds. Set timeoutMs to change it.A test file under claude plugin test <dir>.
describe(name, body)
Documented: An official docs page names this item.
Groups tests. Its name leads the title of each test inside.The body runs at once, while the file loads.Test files.
expect(received, message?)
Documented: An official docs page names this item.
Assertions: Matchers (the toBe family), AsyncMatchers, Negatable (not), and the asymmetric matchers expect.any and expect.objectContaining.Expect, Expectation, Matchers, and Matching type it.Test files.
tier(tier)
Documented: An official docs page names this item.
Sets the tier that the plugin under test loads in. PluginTier excludes core.Call it before the tests.Test files.
mock
Documented: An official docs page names this item.
Memory-backed answers for the nouns beneath the plugins: clock, store, env, session, and more.The testing module types MockClock, MockSession, and the options.Test files.
$.tool.call(input)
Documented: An official docs page names this item.
Raises a tool call as the engine does. agentId names the loop it runs in, a subagent's or a teammate's (new in 2.1.307). Left out, the main loop.A hooked tool.check runs as for a background subagent: a test's classic.PermissionRequest answers an ask, and no test plays a person's yes. Typed by EngineToolCall.Tests only.
$.ui.mount(...)
Documented: An official docs page names this item.
Draws a component through the plugin on the surface that the test names. Returns a handle to read and act on by key.Surfaces: terminal, desktop, vscode, mobile. The handle finds, presses, types, selects, resizes, and unmounts. It tests the mod, not the paint.Tests only.

Errors​

Each row is a family of messages. The backticked text is the start that every message in the family shares. A row marked Various gathers the messages of one feature that do not share one start.

Start of MessageStatusWhat It MeansCases
$.classic., $.${…} is no call, $.${…} in a test
Undocumented: Only the program shows this item. It can change without notice.
A test called a $ call that the test kit does not know.A classic event such as $.classic.PreToolUse is no call on $. A test passes the whole input of its event.
$.ui.
Undocumented: Only the program shows this item. It can change without notice.
A $.ui call in a test found no drawn element, or found more than one.No element of the kind is drawn. The tree holds more than one with the key. In that case, name the requestId or the surface. Render or mount the tree first. An element is mounted already. Or the test ended while the drawing was raised.
test(name, { plugins })
Undocumented: Only the program shows this item. It can change without notice.
A refused plugins option of test().A plugin loads twice. A name is not a plugin name (lowercase letters, digits and dashes). Or a plugin's register is not written register(on) { ... }.
advance(
Undocumented: Only the program shows this item. It can change without notice.
advance() fired timer ticks and more were due.Advance in smaller steps. Or check for an every that the module re-arms on each tick.
claude plugin test:
Undocumented: Only the program shows this item. It can change without notice.
claude plugin test could not run.A folder is not in the plugin, holds no *.test.ts or *.test.tsx files, or holds more test files than the limit.
the kit's
Undocumented: Only the program shows this item. It can change without notice.
The test kit imports a module that is not part of it.The text names the file and the import.
Various
Undocumented: Only the program shows this item. It can change without notice.
Plugin evals. A plugin eval suite or its mocks did not run.The eval directory, a case, a mock, or a fixture is not a plain readable directory or file. Examples are a symbolic link, a hard link, a name that differs only by letter case, or a path that escapes the plugin. A tools:, error:, or abort_when: key appears where it does not apply. Or a saved tools/list response is not valid JSON.