#!/usr/bin/env bash # duplicate-hook-registration-guard.sh 的迴歸測試(inkstone/ISEP#122) # # 這支閘的價值是「不吵的時候真的不吵,該吵的時候真的吵得出正確清單」—— # 兩邊都要驗,而且全程用假的 plugin root/專案目錄,不碰真正的 # InkStoneCo/.claude/ 或這台機器裝著的 ISEP plugin。 set -u HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/duplicate-hook-registration-guard.sh}" PASS=0; FAIL=0; N=0 check() { # check <說明> <輸出> <該出現|!不該出現>... desc="$1"; out="$2"; code="$3"; shift 3; N=$((N+1)); ok=1; why="" if [ "$code" != "0" ]; then ok=0; why="exit 應該是 0(這支從不擋人),實際是 $code"; fi for w in "$@"; do case "$w" in "!"*) if printf '%s' "$out" | grep -qF -- "${w#!}"; then ok=0; why="不該出現卻出現了:${w#!}"; fi ;; *) if ! printf '%s' "$out" | grep -qF -- "$w"; then ok=0; why="少了:$w"; fi ;; esac done if [ "$ok" = 1 ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1)) else printf ' ❌ %s —— %s\n' "$desc" "$why"; printf '%s\n' "$out" | sed 's/^/ /'; FAIL=$((FAIL+1)); fi } # mkroot <名字...> → 造一份假 plugin root,hooks.json 裡真的註冊這些檔名 mkroot() { d="$TMP/root-$RANDOM"; mkdir -p "$d/hooks" { printf '{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[' first=1 for n in "$@"; do [ "$first" = 1 ] || printf ',' printf '{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/hooks/%s"}' "$n" first=0 done printf ']}]}}\n' } > "$d/hooks/hooks.json" printf '%s\n' "$d" } # mkproj <名字...> → 造一份假專案目錄,settings.json 裡登記這些檔名 mkproj() { d="$TMP/proj-$RANDOM"; mkdir -p "$d/.claude" { printf '{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[' first=1 for n in "$@"; do [ "$first" = 1 ] || printf ',' printf '{"type":"command","command":"$CLAUDE_PROJECT_DIR/.claude/hooks/%s"}' "$n" first=0 done printf ']}]}}\n' } > "$d/.claude/settings.json" printf '%s\n' "$d" } run() { # run out=$(CLAUDE_PLUGIN_ROOT="$1" CLAUDE_PROJECT_DIR="$2" bash "$HOOK" 2>&1) code=$? printf '%s\x1e%s' "$out" "$code" } TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT echo "── 該吵 ──────────────────────────────────────────────────────" R=$(mkroot prod-write-guard.sh github-contact-guard.sh) P=$(mkproj prod-write-guard.sh other-thing.sh) o=$(run "$R" "$P"); out="${o%$'\x1e'*}"; code="${o##*$'\x1e'}" check "① 專案版跟 plugin 同名一支 → 點名那一支" "$out" "$code" \ "重複掛載警報" "1 支" "prod-write-guard.sh" "!github-contact-guard.sh" "!other-thing.sh" R=$(mkroot a.sh b.sh c.sh) P=$(mkproj a.sh b.sh c.sh) o=$(run "$R" "$P"); out="${o%$'\x1e'*}"; code="${o##*$'\x1e'}" check "② 全部同名 → 三支都點名" "$out" "$code" "3 支" "a.sh" "b.sh" "c.sh" echo "── 不該吵(誤攔比漏擋嚴重)───────────────────────────────────" R=$(mkroot a.sh) P=$(mkproj z.sh) o=$(run "$R" "$P"); out="${o%$'\x1e'*}"; code="${o##*$'\x1e'}" check "③ 完全不重疊 → 靜音" "$out" "$code" "!重複掛載" R=$(mkroot a.sh) d="$TMP/proj-empty-$RANDOM"; mkdir -p "$d/.claude"; echo '{"hooks":{}}' > "$d/.claude/settings.json" o=$(run "$R" "$d"); out="${o%$'\x1e'*}"; code="${o##*$'\x1e'}" check "④ 專案沒登記任何 .claude/hooks/*.sh → 靜音" "$out" "$code" "!重複掛載" d="$TMP/proj-nosettings-$RANDOM"; mkdir -p "$d/.claude" o=$(run "$R" "$d"); out="${o%$'\x1e'*}"; code="${o##*$'\x1e'}" check "⑤ 專案根本沒有 settings.json → 靜音" "$out" "$code" "!重複掛載" o=$(CLAUDE_PROJECT_DIR="$(mkproj a.sh)" bash "$HOOK" 2>&1); code=$? check "⑥ 沒有 CLAUDE_PLUGIN_ROOT(不是 plugin 環境)→ 靜音" "$o" "$code" "!重複掛載" d="$TMP/root-nohooksjson-$RANDOM"; mkdir -p "$d/hooks" o=$(CLAUDE_PLUGIN_ROOT="$d" CLAUDE_PROJECT_DIR="$(mkproj a.sh)" bash "$HOOK" 2>&1); code=$? check "⑦ plugin root 沒有 hooks.json(環境不完整)→ 靜音" "$o" "$code" "!重複掛載" echo "── 判準是「登記」不是「檔案存在」──────────────────────────────" R=$(mkroot pre-write-guard.template.sh) # 有這個檔但 hooks.json 沒收(樣板慣例) d="$TMP/root-unreg-$RANDOM"; mkdir -p "$d/hooks"; echo '{"hooks":{}}' > "$d/hooks/hooks.json" touch "$d/hooks/pre-write-guard.template.sh" P=$(mkproj pre-write-guard.template.sh) o=$(run "$d" "$P"); out="${o%$'\x1e'*}"; code="${o##*$'\x1e'}" check "⑧ plugin 有這個檔但沒真的註冊 → 不算重複,靜音" "$out" "$code" "!重複掛載" echo "" echo "── 結果:$PASS 通過 / $FAIL 失敗(共 $N)──" [ "$FAIL" -eq 0 ]