Follow Discord
Sweep 22 Oct 2026 · 21:04Z Build v2.1.309 528 read Stable v2.1.308 Latest v2.1.309 Next v2.1.309 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Test a mod changedplugins/mods/test

Nearest release: v2.1.306, published under an hour after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 21 Oct 2026 02:10 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 22 Oct 2026 18:07 UTC.

Upstream edited
Recorded here
Lines+12added
Lines−12removed
From line 2 where the diff opens
First seen 1 Oct 2026 this site's first read of the page
Recorded edits7to this page, all time

The whole hunk

from line 2, old and new numbered
/
lines
from line 2
22 
33> Write automated tests for a Claude Code mod that fire events, stub Claude Code's answers, and press buttons, with no session, sign-in, or network.
44 
5You can write automated tests for a mod and run them from your shell with [`claude plugin test`](/docs/en/plugins/mods/reference#commands). A test fires the events your hooks handle and checks what the hooks did, so you catch a problem before it reaches a session. The first example tests the mod from [Create a mod](/docs/en/plugins/mods/create).
5You can write automated tests for a mod and run them from your shell with [`claude plugin test`](/docs/en/plugins/mods/reference#commands). A test fires the events your hooks handle and checks what the hooks did, so you catch a problem before it reaches a session. The first example tests the mod from [Create a mod](/docs/en/plugins/mods/create.md).
66 
77## Write a test
88 
from line 10
1010 
1111Give each test file a name that ends in `.test.ts`, such as `first-mod.test.ts`, and save it anywhere in the plugin directory. Every test file needs at least one `test()`, or the run fails with `declares no test(): nothing ran`. A test file can import your mod's own files and sibling `.ts` helpers, so you can unit test plain functions, such as a game's rules, without the kit.
1212 
13This test fires two tool calls, runs the `/tally` command from [Create a mod](/docs/en/plugins/mods/create), and checks that the reply counts both. Its first line is a [stub](#stub-what-claude-code-would-answer), which answers the tool calls in Claude Code's place. Save it as `first-mod/tests/first-mod.test.ts`:
13This test fires two tool calls, runs the `/tally` command from [Create a mod](/docs/en/plugins/mods/create.md), and checks that the reply counts both. Its first line is a [stub](#stub-what-claude-code-would-answer), which answers the tool calls in Claude Code's place. Save it as `first-mod/tests/first-mod.test.ts`:
1414 
1515```typescript first-mod/tests/first-mod.test.ts theme={null}
1616import { expect, test } from 'claude-code/testing'
from line 54
5454 
5555No model, store, or tool runs in a test, so wherever your mod expects Claude Code to answer, the test supplies the answer with a stub. A test function receives two arguments for that:
5656 
57* **`$`**: the test's own `$`, which acts as Claude Code. It isn't the [mods API](/docs/en/plugins/mods/reference#mods-api-methods) that a hook receives. Each of its methods fires the event of the same name, sends it through your mod's hooks, and resolves to the result: `$.tool.call({ tool: 'Bash', command: 'ls' })` fires `tool.call`. `$.command.run`, `$.prompt.submit`, `$.session.start`, and `$.turn.complete` work the same way, and `$.classic.Stop` and the other `$.classic` methods fire a [settings hook event](/docs/en/plugins/mods/events#hook-the-settings-hook-events). A test can't fire a mods API call such as `ui.close` directly. Trigger it through your mod, for example by pressing the button that closes the pane.
57* **`$`**: the test's own `$`, which acts as Claude Code. It isn't the [mods API](/docs/en/plugins/mods/reference.md#mods-api-methods) that a hook receives. Each of its methods fires the event of the same name, sends it through your mod's hooks, and resolves to the result: `$.tool.call({ tool: 'Bash', command: 'ls' })` fires `tool.call`. `$.command.run`, `$.prompt.submit`, `$.session.start`, and `$.turn.complete` work the same way, and `$.classic.Stop` and the other `$.classic` methods fire a [settings hook event](/docs/en/plugins/mods/events.md#hook-the-settings-hook-events). A test can't fire a mods API call such as `ui.close` directly. Trigger it through your mod, for example by pressing the button that closes the pane.
5858* **`on`**: call it to register stubs, which are hooks that answer in Claude Code's place. Name a stub for a mods API call without the `$.`, so a stub registered as `store.get` answers your mod's `$.store.get`. When your mod calls [`$.model.complete`](/docs/en/plugins/mods/api#call-a-model) or [`$.store.get`](/docs/en/plugins/mods/interface#keep-state), a stub supplies the answer.
5959 
60This example stubs a model call. The hook belongs to a mod named `grader`, and handles a `/grade` command that sends a sentence to a model and reports whether the reply starts with `PASS`. The file holds only the hook under test, so the mod also needs a `plugin.json` and a `hooks.json`, as in [Create a mod](/docs/en/plugins/mods/create#write-a-mod-yourself). To type `/grade` in a session, the mod also has to [register the command](/docs/en/plugins/mods/api#add-a-command):
60This example stubs a model call. The hook belongs to a mod named `grader`, and handles a `/grade` command that sends a sentence to a model and reports whether the reply starts with `PASS`. The file holds only the hook under test, so the mod also needs a `plugin.json` and a `hooks.json`, as in [Create a mod](/docs/en/plugins/mods/create.md#write-a-mod-yourself). To type `/grade` in a session, the mod also has to [register the command](/docs/en/plugins/mods/api.md#add-a-command):
6161 
6262```javascript grader/hooks/register.js theme={null}
6363export function register(on) {
from line 126
126126 await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })
127127 ```
128128 
129 The second stub answers the `$.command.register` call that a `session.start` hook such as the [tutorial's](/docs/en/plugins/mods/create#write-a-mod-yourself) makes. Without it, that call rejects with `no implementation for command.register` and the kit skips your hook, so nothing after the call in the hook runs. The test doesn't fail at that point. The skipped hook is listed under `the engine reported:` only if a later check fails.
129 The second stub answers the `$.command.register` call that a `session.start` hook such as the [tutorial's](/docs/en/plugins/mods/create.md#write-a-mod-yourself) makes. Without it, that call rejects with `no implementation for command.register` and the kit skips your hook, so nothing after the call in the hook runs. The test doesn't fail at that point. The skipped hook is listed under `the engine reported:` only if a later check fails.
130130 
131131* **A hook that returns `next(e)` needs a stub to answer.** When your [`ui.render`](/docs/en/plugins/mods/reference#interface) hook returns `next(e)`, for example to draw nothing while Claude is idle, [mounting it](#test-a-drawing) fails with `no implementation for ui.render`. Register a stub that returns an element as plain data:
132132 
from line 255
255255 
256256## Test a drawing
257257 
258A test can draw one of your mod's [render sites](/docs/en/plugins/mods/reference#render-sites), then press, type into, and find the elements it drew. `$.ui.mount` draws the site through your mod's `ui.render` hook and returns a handle with a method for each of those. To cover several apps in one test, set `surface` to the app to draw for. This test opens the pane from [Build a pane with tabs](/docs/en/plugins/mods/interface#build-a-pane-with-tabs), switches tabs, presses the button, and checks the count in the terminal and the Desktop app:
258A test can draw one of your mod's [render sites](/docs/en/plugins/mods/reference.md#render-sites), then press, type into, and find the elements it drew. `$.ui.mount` draws the site through your mod's `ui.render` hook and returns a handle with a method for each of those. To cover several apps in one test, set `surface` to the app to draw for. This test opens the pane from [Build a pane with tabs](/docs/en/plugins/mods/interface.md#build-a-pane-with-tabs), switches tabs, presses the button, and checks the count in the terminal and the Desktop app:
259259 
260260```typescript hello-tabs/tests/hello-tabs.test.ts theme={null}
261261import { expect, test } from 'claude-code/testing'
from line 313
313313| `find({ key: 'more' })` or `find({ type: 'Text', text: 'Count: 2' })` | Returns the first matching element as `{ type, props, children }`, or `undefined`. `text` can be a string or a regular expression. |
314314| `unmount()` | Removes the drawing |
315315 
316Each method resolves after your handler has finished, so you can check the result on the next line. Set `props` to what Claude Code would pass for that site. The [render sites table](/docs/en/plugins/mods/reference#render-sites) lists each site's props, and [the types for your build](/docs/en/plugins/mods/create#get-the-types-for-your-build) have their types.
316Each method resolves after your handler has finished, so you can check the result on the next line. Set `props` to what Claude Code would pass for that site. The [render sites table](/docs/en/plugins/mods/reference.md#render-sites) lists each site's props, and [the types for your build](/docs/en/plugins/mods/create.md#get-the-types-for-your-build) have their types.
317317 
318318A drawing test checks the tree your hook returns and whether it's valid for that app. It doesn't check how the app paints it, so look at a new layout in a real session as well.
319319 
from line 321
321321 
322322Each test starts with every `$.state` value at its default, which is how `/clear` leaves them. To test what your mod does next, skip `session.start`, fire `classic.SessionStart` with `source: 'clear'`, and check what your mod draws.
323323 
324This test checks the module from [Load a saved value again after `/clear`](/docs/en/plugins/mods/interface#load-a-saved-value-again-after-clear). Add it to the file from [Test a drawing](#test-a-drawing), where `PANE` is defined. That file's first test expects the button to save the count, as the button in [Save from more than one session](/docs/en/plugins/mods/interface#save-from-more-than-one-session) does:
324This test checks the module from [Load a saved value again after `/clear`](/docs/en/plugins/mods/interface#load-a-saved-value-again-after-clear). Add it to the file from [Test a drawing](#test-a-drawing), where `PANE` is defined. That file's first test expects the button to save the count, as the button in [Save from more than one session](/docs/en/plugins/mods/interface.md#save-from-more-than-one-session) does:
325325 
326326```typescript hello-tabs/tests/hello-tabs.test.ts theme={null}
327327test('the saved count comes back after /clear', async ($, on) => {
from line 346
346346 
347347A mod your organization lists in [`prependPlugins`](/docs/en/plugins/mods/admin) can refuse another mod before it loads. To test one, set your mod's tier and give the test a second mod for yours to allow or refuse:
348348 
349* **`tier`**: call it once at the top of the test file, as in `tier('prepend')`, to load your mod as `prepend`, `append`, or `builtin`, its place in the [order mods run in](/docs/en/plugins/mods/events#the-order-mods-run-in). Without it, your mod loads as `user`.
349* **`tier`**: call it once at the top of the test file, as in `tier('prepend')`, to load your mod as `prepend`, `append`, or `builtin`, its place in the [order mods run in](/docs/en/plugins/mods/events.md#the-order-mods-run-in). Without it, your mod loads as `user`.
350350* **`plugins`**: pass `test` an options object ahead of the test body. Its `plugins` array holds mods you write inline, each with a `name` and a `register` function. To load one somewhere other than `user`, add `tier` to it.
351351 
352This test file loads the [policy mod from the admin page](/docs/en/plugins/mods/admin#enforce-a-policy-with-a-mod-of-your-own) first. It checks that the policy mod refuses a mod that starts a process and allows one that doesn't:
352This test file loads the [policy mod from the admin page](/docs/en/plugins/mods/admin.md#enforce-a-policy-with-a-mod-of-your-own) first. It checks that the policy mod refuses a mod that starts a process and allows one that doesn't:
353353 
354354```typescript acme-guard/tests/guard.test.ts theme={null}
355355import { expect, test, tier } from 'claude-code/testing'
from line 404
404404 
405405## Next steps
406406 
407* [Troubleshoot a mod](/docs/en/plugins/mods/troubleshoot): find out why a mod does nothing in a session
408* [Mods reference](/docs/en/plugins/mods/reference): every event's input and result, for writing stubs
407* [Troubleshoot a mod](/docs/en/plugins/mods/troubleshoot.md): find out why a mod does nothing in a session
408* [Mods reference](/docs/en/plugins/mods/reference.md): every event's input and result, for writing stubs
409409 
Feedback