#!/bin/sh # Tests for hooks/pre-task-ritual.sh — the UserPromptSubmit hook that surfaces # rule 0 on every prompt (#44). # # WHY THIS EXISTS, AND WHAT IT DELIBERATELY DOES NOT CLAIM # This hook is three lines and echoes a string. It has no bash-isms, so it # cannot fail the way secret-guard appeared to; that risk was overstated when # this test was requested. What it CAN do is stop working silently, and its # whole job is to be visible -- if the text stops appearing, rule 0 stops being # surfaced on every prompt and nothing announces that. # # THE ASSERTION THAT MATTERS IS REGISTRATION. # A hook file that exists but is not in hooks.json does absolutely nothing, and # looks identical on disk to one that works. So this derives the command from # hooks.json rather than assuming one: the registration is the source of truth # for how the hook is invoked, and a test that invokes it its own way is # testing something the harness never runs. # # That distinction is not hypothetical. secret_guard_test.sh piped its hook into # `sh` while hooks.json runs it with `bash`; on Debian /bin/sh is dash, which # has no `set -o pipefail`, and the suite failed 20/20 in CI against a hook that # was fine in production. The test was wrong, not the hook. Deriving the # invocation removes the opportunity for that class of mistake entirely. set -u ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) HOOK="$ROOT/hooks/pre-task-ritual.sh" REG="$ROOT/hooks/hooks.json" pass=0 fail=0 ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; } no() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; } if ! command -v jq >/dev/null 2>&1; then printf ' SKIP: jq not installed -- cannot read the hook registration\n' exit 0 fi [ -f "$HOOK" ] || { printf ' FAIL %s does not exist\n' "$HOOK"; exit 1; } ok 'hook file exists' [ -f "$REG" ] || { printf ' FAIL %s does not exist\n' "$REG"; exit 1; } # 1. Registered, and on the right event. UserPromptSubmit is what makes it fire # on every prompt; registered under any other event it would still "exist" # and still do nothing useful. CMD=$(jq -r ' .hooks.UserPromptSubmit[]?.hooks[]? | select(.command | test("pre-task-ritual")) | .command ' "$REG" 2>/dev/null | head -1) if [ -n "$CMD" ]; then ok 'registered on UserPromptSubmit in hooks.json' else no 'not registered on UserPromptSubmit -- the file would exist and never run' printf ' %d passed, %d failed\n' "$pass" "$fail" exit 1 fi # 2. Run it EXACTLY as registered, with the harness variable substituted the way # the harness substitutes it. OUT=$(CLAUDE_PLUGIN_ROOT="$ROOT" sh -c "$CMD" 2>&1) rc=$? [ "$rc" -eq 0 ] && ok 'exits 0 as registered' \ || no "exited $rc -- a non-zero UserPromptSubmit hook can block the prompt" # 3. The content is the point. A hook that runs and says nothing is the same as # no hook, and that is the failure this pins. if [ -n "$OUT" ]; then ok 'produces output' else no 'produced no output -- rule 0 would silently stop being surfaced' fi case "$OUT" in *"Rule 0"*) ok 'names rule 0' ;; *) no "does not name rule 0: '$OUT'" ;; esac # Each of the four steps, individually. Naming rule 0 while having quietly lost # a step is the plausible drift, not losing the whole line. for step in brain_query skill 'failing test' 'success criterion'; do case "$OUT" in *"$step"*) ok "states the '$step' step" ;; *) no "lost the '$step' step from the ritual" ;; esac done # 4. The interpreter named in the registration must actually exist, or the hook # fails on that host with a message nobody reads. INTERP=${CMD%% *} if command -v "$INTERP" >/dev/null 2>&1; then ok "registered interpreter '$INTERP' is available" else no "registered interpreter '$INTERP' not found on this host" fi printf ' %d passed, %d failed\n' "$pass" "$fail" [ "$fail" -eq 0 ]