voyage/tests/hooks/storm-matcher.test.mjs
Kjell Tore Guttormsen 2ee8137e7c
feat(hooks): wave-1 guards -- approval ask, plan gate, test lock, post-edit check
Wave-1 order 4 (P7, P12, the P9 test lock, the P3 plan-approval guard).
All command hooks; none runs a model.

- P12: pre-bash-executor answers permissionDecision "ask" before merge
  into main, push, tag/publish and destructive git (holds in auto mode).
  post-approval-log writes exactly what the person was asked to the K2
  approval log: merge/push to main as `merge`, tag/publish as `release`,
  with a one-hour expiry; nothing when no person was asked.
- P3: pre-plan-gate runs the K3 plan gate on a Voyage plan leaving plan
  mode and sends a refused plan back; "not measured" asks, never passes.
- P9: lib/util/test-lock.mjs (K8). pre-write-executor refuses edits to
  locked test files; stop-test-lock refuses "done" for the locking
  session (stop_hook_active guarded).
- P7: post-edit-check runs the target repo's own formatter/linter per
  edit, only during a Voyage plan run or after opt-in
  (.claude/voyage-guards.json); lib/verification/step-check.mjs (K9) fails
  the step on findings; no tool reads "not measured", never green.
- STORM cap matcher also names `Agent` (the tool's name since 2.1.63).
- Mod precondition stated in README, SECURITY.md and every PreToolUse stop
  guard: Voyage's guards are deterministic only when no installed mod
  handles `tool.check`.

K2, K3 and K5 are stubbed in tests until orders 2 and 3 are merged.
Two independent review rounds; every finding has a regression test.
Suite 1600 (1598 pass / 0 fail / 2 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:09:32 +02:00

26 lines
1.3 KiB
JavaScript

// tests/hooks/storm-matcher.test.mjs
// The STORM cap (pre-agent-cap.mjs) must see subagent spawns. The tool was
// renamed Task -> Agent in Claude Code 2.1.63; the docs state the old name
// still works as an alias «in settings and agent definitions»
// (code.claude.com/docs/en/sub-agents, the rename note) and say nothing about
// hook matchers, which compare against the tool name the hook input carries.
// So the matcher must name `Agent` itself. Resolved with Claude Code's matcher
// rules (hooks § Matcher patterns), as write-guard-coverage.test.mjs does.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { scriptsFor } from './guard-helpers.mjs';
const fires = (tool) => scriptsFor('PreToolUse', tool).some((p) => p.endsWith('pre-agent-cap.mjs'));
test('STORM cap fires on Agent (the tool name since 2.1.63)', () => {
assert.ok(fires('Agent'), 'pre-agent-cap.mjs must fire for tool_name "Agent"');
});
test('STORM cap still fires on WebSearch, WebFetch and the old Task name', () => {
for (const t of ['WebSearch', 'WebFetch', 'Task']) assert.ok(fires(t), t);
});
test('STORM cap does not fire on unrelated tools', () => {
for (const t of ['Bash', 'Edit', 'Write', 'Read', 'AgentOutput', 'ExitPlanMode']) assert.ok(!fires(t), t);
});