[FEATURE] claude plugin test: let a test raise or pause the per-hook time budget

Status Open
Reported on v2.1.295
Maintainer reply None cached
Activity 0 comments · opened Oct 10, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

In claude plugin test, each hook has a fixed 10,000 ms budget. A test cannot change it, and the test's own timeoutMs does not reach it. Per the mods Limits table, the budget pauses only while a hook awaits next() or a mods API call.

A test's bottom stub hook has no next(), so any wait in the stub counts against the budget. That makes a long-running tool hard to test. For example, a mod that reacts to a call still running after N minutes needs a stub that holds the call open while the test advances mock.clock. Advancing the mock clock through a mod's $.clock.every timer takes real time. Past 10 s the engine skips the stub:

test's tool.call hook was skipped: test: exceeded 10000ms budget
HooksError: no implementation for tool.call

On 2.1.295 this trips at about 10.1 s on an idle machine. The stub also cannot call $.clock.sleep. The host refuses it unless the hooks module under test calls it:

test's tool.call hook was skipped: test: $.clock.sleep refused: its hooks module does not call it

Proposed Solution

Either of these would solve it:

  1. A per-test option for the hook budget of the test's own hooks, for example test(name, { hookBudgetMs: 60000 }, body).
  2. Pause a test hook's budget while mock.clock is advancing, because mock time and wall time are unrelated there.

Alternative Solutions

  • Raising the test's timeoutMs does not help: the hook budget fires first.
  • Shorter simulated waits weaken the test.
  • Today we accept the ceiling and leave a comment at the stub.

Priority

Low - Nice to have

Feature Category

Developer tools/SDK

Use Case Example

Minimal repro, a scratch plugin with a pass-through mod:

.claude-plugin/plugin.json   { "name": "budget-repro", "version": "0.0.1" }
hooks/hooks.json             { "modules": ["./mod.ts"] }
hooks/mod.ts                 export const register: Register = (on) => { on('tool.call', ($, e, next) => next(e)) }

hooks/mod.test.tsx:

import { expect, test } from 'claude-code/testing'

test('stub waits 11 s', { timeoutMs: 60000 }, async ($, on) => {
  on('tool.call', async () => {
    await new Promise((r) => setTimeout(r, 11000)) // a slow tool
    return { result: { stdout: 'ok', stderr: '', interrupted: false } }
  })
  const r: any = await $.tool.call({ tool: 'Bash', command: 'x' } as any)
  expect(r?.result?.stdout).toBe('ok')
})

claude plugin test . result:

(fail) stub waits 11 s [10091.03ms]
HooksError: no implementation for tool.call
  test's tool.call hook was skipped: test: exceeded 10000ms budget

The real case was the same shape with mock.clock: a stub held the call open while the test ran clock.advance(3 days) against a mod with a 5 s $.clock.every timer. It failed at 10095.60 ms.

Additional Context

Claude Code 2.1.295. Related umbrella: #91870 (Mods).

View original on GitHub ↗