1# Chapter 5: tests, a daemon made of cardboard 2 3How do you test code that only runs inside Claude Code, talks to a daemon 4over a Unix socket, and asks a model on the internet for its opinion? You 5replace everything it touches with something you control, and watch what it 6tries to do. 7 8Claude Code ships a test runner for mods (`claude plugin test`, with 9`claude-code/testing`). In a test, the mod's `$` calls are hooks too, so a 10test can register its own handler for `http.fetch` and be the daemon, or 11for `session.id` and be the session. The test file's `host(on, answer)` does 12exactly that: 13 14- `http.fetch` records the path and body of every request, then answers 15 `/decide` with `answer`, and anything else with `{}`. Pass `undefined` 16 and it throws `connect ENOENT`, which is what a missing socket looks like. 17- `env.get`, `session.id`, `session.cwd`, `session.root` give a fixed home, 18 session and project (`/home/test`, `session-1`, `/work`). 19- `classic.PreToolUse`, `classic.Stop`, `classic.UserPromptSubmit` and 20 `tool.call` stand in for whatever would sit beneath the plugin, so `next(e)` 21 has something to call. 22 23Then a test drives the mod the way Claude Code would (`$.tool.call`, 24`$.classic.Stop`, `$.ui.mount`) and looks at what was sent and what came 25back. 26 27| Test | What it pins | 28| --- | --- | 29| a plainly read-only command is never sent to the daemon | `git status` makes no request at all. | 30| a risky command is sent to the daemon with its session and directories | `rm -rf target` makes one `/decide` with the event, the session, the root and the command. | 31| a tool other than Bash is not judged | A `Read` makes no request. | 32| a turn that Jev says stopped early is sent back with the reason | An `ask` on `Stop` becomes a `block` carrying Jev's line. | 33| a turn ends as usual when Jev passes, and when the daemon is unreachable | No daemon, no block. | 34| a submitted prompt is reported to the daemon without being answered | `UserPromptSubmit` sends one `/observe`, after the hook has already returned. | 35| the band shows the last decision (terminal), (desktop) | After a judged command, the band above the prompt has the line, on both surfaces. | 36 37> **Aside: the test that has to wait.** Most of these finish when the hook 38> returns. The prompt test cannot: the mod sends `/observe` without 39> waiting, on purpose, so the hook has returned before the request leaves. 40> The test polls for up to half a second (50 times 10 ms) using a 41> `setTimeout` that exists in the test file's world, and not in the mod's. 42> That is why the file declares `setTimeout` itself. 43 44> **Try it.** 45> 46> claude plugin test plugin 47> 48> from the repository root: eight tests, about five seconds, no daemon, no 49> key and no Rust, because the generated `jevhooks.js` is committed. Break 50> something on purpose (make `classic.Stop` return `beneath` always) and 51> watch the stopped-early test fail. 52 53## For the people who maintain it 54 55The fake `answer` lines are written to look like the daemon's, but they are 56only strings: the tests check the mod's plumbing, not Jev's wording. The 57verdict rules are tested in Rust (`crates/jevhooks-daemon/src/judge.rs`, 58chapter 12); the routing in `crates/jevhooks-mod` (chapter 10). 59 60### In this folder 61 62| Path | What | 63| --- | --- | 64| [jevhooks.test.tsx](jevhooks.test.tsx) | The stand-in host and daemon, and the eight tests. | 65 66← Previous: [Chapter 4, types/](../types/) · Up: [plugin](../) · Next: [Chapter 6, crates/](../../crates/) →