119 lines
7.7 KiB
Bash
Executable File
119 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Test harness for the ADR-003 consultation gate hooks.
|
|
# Pipes simulated Claude Code hook JSON into sap-consult-gate.sh and
|
|
# mark-consult.sh with a temp marker (SAP_CONSULT_MARKER) so the real
|
|
# .claude/.last-mcp-consult is never touched.
|
|
#
|
|
# Usage: bash .claude/hooks/tests/run.sh (exit 0 = all pass)
|
|
|
|
set -u
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
HOOKS="$(dirname "$HERE")"
|
|
REPO="$(cd "$HOOKS/../.." && pwd)"
|
|
GATE="$HOOKS/sap-consult-gate.sh"
|
|
MARK="$HOOKS/mark-consult.sh"
|
|
|
|
TMP="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
STALE="$TMP/stale-marker"
|
|
FRESH="$TMP/fresh-marker"
|
|
echo $(( $(date +%s) - 7200 )) > "$STALE"
|
|
date +%s > "$FRESH"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
# gate MARKER JSON -> prints "deny" or "allow"; also asserts exit 0 + valid JSON.
|
|
gate() {
|
|
local marker="$1" json="$2" out rc
|
|
out="$(printf '%s' "$json" | SAP_CONSULT_MARKER="$marker" CLAUDE_PROJECT_DIR="$REPO" bash "$GATE")"
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then echo "rc=$rc"; return; fi
|
|
if [ -n "$out" ] && ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then echo "badjson"; return; fi
|
|
if printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny"' >/dev/null 2>&1; then
|
|
echo deny
|
|
else
|
|
echo allow
|
|
fi
|
|
}
|
|
|
|
expect() {
|
|
local name="$1" want="$2" got="$3"
|
|
if [ "$want" = "$got" ]; then
|
|
PASS=$((PASS + 1)); echo "PASS $name [$got]"
|
|
else
|
|
FAIL=$((FAIL + 1)); echo "FAIL $name [want $want, got $got]"
|
|
fi
|
|
}
|
|
|
|
j_edit() { printf '{"tool_name":"Edit","tool_input":{"file_path":"%s","old_string":"a","new_string":"b"}}' "$1"; }
|
|
j_bash() { printf '{"tool_name":"Bash","tool_input":{"command":%s}}' "$(printf '%s' "$1" | jq -Rs .)"; }
|
|
j_fswr() { printf '{"tool_name":"mcp__filesystem__write_file","tool_input":{"path":"%s","content":"x"}}' "$1"; }
|
|
j_fsmv() { printf '{"tool_name":"mcp__filesystem__move_file","tool_input":{"source":"%s","destination":"%s"}}' "$1" "$2"; }
|
|
j_nb() { printf '{"tool_name":"NotebookEdit","tool_input":{"notebook_path":"%s","new_source":"x"}}' "$1"; }
|
|
|
|
MANIFEST="$REPO/app/webapp/manifest.json"
|
|
ANNOT="$REPO/app/webapp/annotations/annotation.xml"
|
|
|
|
echo "== sap-consult-gate.sh =="
|
|
# --- Cases required by the brief ---------------------------------------------
|
|
expect "Edit manifest.json, stale marker => deny" deny "$(gate "$STALE" "$(j_edit "$MANIFEST")")"
|
|
expect "Edit manifest.json, fresh marker => allow" allow "$(gate "$FRESH" "$(j_edit "$MANIFEST")")"
|
|
expect "Bash sed -i manifest.json, stale => deny" deny "$(gate "$STALE" "$(j_bash "sed -i 's/a/b/' app/webapp/manifest.json")")"
|
|
expect "Bash cat manifest.json, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json")")"
|
|
expect "Edit worker/src/x.ts, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/worker/src/x.ts")")"
|
|
expect "Edit PRD.md, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/PRD.md")")"
|
|
expect "fs write_file annotation.xml, stale => deny" deny "$(gate "$STALE" "$(j_fswr "$ANNOT")")"
|
|
|
|
# missing jq: empty PATH, fresh marker, gated file -> must still deny, naming jq.
|
|
out="$(printf '%s' "$(j_edit "$MANIFEST")" | env -i PATH=/nonexistent SAP_CONSULT_MARKER="$FRESH" /bin/bash "$GATE")"; rc=$?
|
|
if [ "$rc" -eq 0 ] && printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny" and (.hookSpecificOutput.permissionDecisionReason | test("jq"))' >/dev/null 2>&1; then
|
|
got=deny; else got="allow(rc=$rc)"; fi
|
|
expect "missing jq (PATH stripped) => deny naming jq" deny "$got"
|
|
|
|
# --- Additional coverage --------------------------------------------------------
|
|
expect "Bash sed -i manifest.json, fresh => allow" allow "$(gate "$FRESH" "$(j_bash "sed -i 's/a/b/' app/webapp/manifest.json")")"
|
|
expect "Bash sed -n (read-only) on manifest, stale => allow" allow "$(gate "$STALE" "$(j_bash "sed -n '1,20p' app/webapp/manifest.json")")"
|
|
expect "Bash grep on app/ path, stale => allow" allow "$(gate "$STALE" "$(j_bash "grep -rn foo app/webapp/")")"
|
|
expect "Bash cat with 2>/dev/null, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json 2>/dev/null")")"
|
|
expect "Bash cat 2>&1 | head, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json 2>&1 | head -5")")"
|
|
expect "Bash heredoc > .cds, stale => deny" deny "$(gate "$STALE" "$(j_bash "cat > srv/schema.cds <<'X'
|
|
entity A {}
|
|
X")")"
|
|
expect "Bash echo >> manifest.json, stale => deny" deny "$(gate "$STALE" "$(j_bash "echo '{}' >> app/webapp/manifest.json")")"
|
|
expect "Bash tee annotation.xml, stale => deny" deny "$(gate "$STALE" "$(j_bash "printf x | tee app/webapp/annotations/annotation.xml")")"
|
|
expect "Bash cp into app/, stale => deny" deny "$(gate "$STALE" "$(j_bash "cp /tmp/x.json app/webapp/manifest.json")")"
|
|
expect "Bash mv within app/, stale => deny" deny "$(gate "$STALE" "$(j_bash "mv app/webapp/a.xml app/webapp/b.xml")")"
|
|
expect "Bash python -c writing .abap, stale => deny" deny "$(gate "$STALE" "$(j_bash "python3 -c 'open(\"src/zcl_x.clas.abap\",\"w\").write(\"x\")'")")"
|
|
expect "Bash node -e writing manifest, stale => deny" deny "$(gate "$STALE" "$(j_bash "node -e 'require(\"fs\").writeFileSync(\"app/webapp/manifest.json\",\"{}\")'")")"
|
|
expect "Bash echo > worker file, stale => allow" allow "$(gate "$STALE" "$(j_bash "echo x > worker/src/x.ts")")"
|
|
expect "Bash echo > docs file, stale => allow" allow "$(gate "$STALE" "$(j_bash "echo x > docs/adr/ADR-009.md")")"
|
|
expect "Bash npm test (no write), stale => allow" allow "$(gate "$STALE" "$(j_bash "cd worker && npm test")")"
|
|
expect "Edit .bdef, stale => deny" deny "$(gate "$STALE" "$(j_edit "$REPO/src/zi_x.bdef")")"
|
|
expect "Edit fragment.xml, stale => deny" deny "$(gate "$STALE" "$(j_edit "$REPO/app/webapp/ext/X.fragment.xml")")"
|
|
expect "Edit app/README.md (exempt ext), stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/app/README.md")")"
|
|
expect "Edit .claude/hooks file, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/.claude/hooks/x.sh")")"
|
|
expect "NotebookEdit under app/, stale => deny" deny "$(gate "$STALE" "$(j_nb "$REPO/app/x.ipynb")")"
|
|
expect "fs move_file src gated, stale => deny" deny "$(gate "$STALE" "$(j_fsmv "$MANIFEST" "/tmp/m.json")")"
|
|
expect "fs move_file dest gated, stale => deny" deny "$(gate "$STALE" "$(j_fsmv "/tmp/m.json" "$MANIFEST")")"
|
|
expect "Edit with no path field => allow" allow "$(gate "$STALE" '{"tool_name":"Edit","tool_input":{}}')"
|
|
expect "Corrupt marker content => deny" deny "$(printf 'garbage' > "$TMP/bad"; gate "$TMP/bad" "$(j_edit "$MANIFEST")")"
|
|
expect "Missing marker file => deny" deny "$(gate "$TMP/does-not-exist" "$(j_edit "$MANIFEST")")"
|
|
|
|
echo "== mark-consult.sh =="
|
|
M="$TMP/marked"
|
|
printf '{"tool_name":"mcp__sap-docs__search","tool_response":{}}' | SAP_CONSULT_MARKER="$M" CLAUDE_PROJECT_DIR="$REPO" bash "$MARK"; rc=$?
|
|
expect "mark-consult exits 0" 0 "$rc"
|
|
now=$(date +%s); v="$(cat "$M" 2>/dev/null || echo none)"
|
|
case "$v" in *[!0-9]*|'') got=bad ;; *) [ $((now - v)) -le 5 ] && got=fresh || got=old ;; esac
|
|
expect "mark-consult writes epoch to override marker" fresh "$got"
|
|
expect "gate allows after mark-consult stamped marker" allow "$(gate "$M" "$(j_edit "$MANIFEST")")"
|
|
[ -f "$REPO/.claude/.last-mcp-consult" ] && before="$(cat "$REPO/.claude/.last-mcp-consult")" || before="absent"
|
|
[ -f "$REPO/.claude/.last-mcp-consult" ] && after="$(cat "$REPO/.claude/.last-mcp-consult")" || after="absent"
|
|
expect "real marker untouched by harness" "$before" "$after"
|
|
|
|
echo
|
|
echo "passed=$PASS failed=$FAIL"
|
|
[ "$FAIL" -eq 0 ]
|