diff --git a/tests/pre_task_ritual_test.sh b/tests/pre_task_ritual_test.sh new file mode 100644 index 0000000..7f82862 --- /dev/null +++ b/tests/pre_task_ritual_test.sh @@ -0,0 +1,103 @@ +#!/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 ]