voyage/tests/lib/guards-prose-pins.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

71 lines
3.4 KiB
JavaScript

// tests/lib/guards-prose-pins.test.mjs
// Prose pins owned by the guards process (wave-1 order 4). Counted as doc pins
// by lib/util/test-census.mjs (PIN_FILE_RE matches `prose-pins.test.mjs`).
//
// The mod precondition: a Claude Code mod that handles `tool.check` answers
// AFTER the rules and the PreToolUse hooks, and «its answer can replace
// theirs» — it can approve a call a plugin PreToolUse hook blocked
// (code.claude.com/docs/en/permissions § Extend permissions with hooks,
// verified verbatim 06.10). So Voyage's stop guards are deterministic only
// under a stated condition, and the condition is written where the guards are
// described: README § Security hardening, SECURITY.md, and the head of every
// PreToolUse stop guard.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
export const MOD_PRECONDITION = "Voyage's guards are deterministic only when no installed mod handles `tool.check`.";
const STOP_GUARDS = [
'hooks/scripts/pre-bash-executor.mjs',
'hooks/scripts/pre-write-executor.mjs',
'hooks/scripts/pre-agent-cap.mjs',
'hooks/scripts/pre-plan-gate.mjs',
];
test('mod precondition: README § Security hardening states it', () => {
const md = read('README.md');
const start = md.indexOf('### Security hardening');
assert.ok(start !== -1);
const end = md.indexOf('\n### ', start + 1);
assert.ok(md.slice(start, end).includes(MOD_PRECONDITION), 'README § Security hardening must state the mod precondition');
});
test('mod precondition: SECURITY.md states it', () => {
assert.ok(read('SECURITY.md').includes(MOD_PRECONDITION));
});
test('mod precondition: the head of every PreToolUse stop guard states it', () => {
for (const rel of STOP_GUARDS) {
const head = read(rel).split('\n').slice(0, 40).join('\n').replace(/^\/\/ ?/gm, '').replace(/\n/g, ' ');
assert.ok(head.replace(/\s+/g, ' ').includes(MOD_PRECONDITION), `${rel}: the first 40 lines must state the mod precondition`);
}
});
test('mod precondition: the guards cannot see a mod; the docs say so where it is stated', () => {
// Looked up 06.10: no hook input field names installed mods; the documented
// ways to see them are `/plugin` and a CLI subcommand, both for a person.
assert.match(read('SECURITY.md'), /cannot tell whether such a mod is installed/);
});
test('P12 guard: README names the classes it asks about and that it holds in auto mode', () => {
const md = read('README.md');
const sec = md.slice(md.indexOf('### Security hardening'), md.indexOf('#### Recommended: disable Skill shell execution'));
for (const w of ['merge into `main`', 'push', 'tag', 'destructive git', 'auto mode']) {
assert.ok(sec.includes(w), `README § Security hardening must mention ${w}`);
}
});
test('borrowings are credited where the guards are described (PLAN fixed limit 3)', () => {
const md = read('README.md');
const sec = md.slice(md.indexOf('### Security hardening'), md.indexOf('#### Recommended: disable Skill shell execution'));
for (const src of ['Superpowers', 'agent-skills', 'ai-native-sdlc', 'Pocock', 'Kiro', 'Cherny', 'Claude Code']) {
assert.ok(sec.includes(src), `credit missing for ${src}`);
}
});