Files
ISEP/hooks/tests/duplicate-hook-registration-guard.test.sh
T
Leo ee085c1854 修好一次性戳記恆擋:InkStoneCo 專案版 hooks 與 plugin 雙掛載
inkstone/ISEP#122:leo 三次要部署被 prod-write-guard.sh 恆擋,蓋了戳記
也放不了行。根因不在 prod-write-guard.sh 本身,而在
InkStoneCo/.claude/settings.json 還登記著一份 2026-08-20 立 ISEP plugin
之前的舊 hook,跟 plugin 自己的 hooks.json 同時掛在同一個事件上——每個
PreToolUse 都跑了兩次,一次性戳記被第一份消耗掉,第二份永遠看到空戳記。

實查(在這台機器的樹上實數):InkStoneCo 專案版登記著 38 支 hook,
34 支跟 plugin 完全同名同用途(純殘骸)、3 支是已退役機制
(claim-verify-police.sh/subagent-claim-worksheet.sh/sdd-guard.sh,
ISEP#60/#91 早已裁定退役但專案版沒跟著退)、1 支(kv-write-guard.sh)
是專案版有、plugin 當時沒有的真閘。

這次:
- kv-write-guard.sh 原樣搬進本 repo(B 組,PreToolUse Write|Edit|MultiEdit),
  補齊那個真的缺口,附 8 條迴歸測試
- 新增 duplicate-hook-registration-guard.sh(E 組,SessionStart):
  往後任何專案的 settings.json 又跟 plugin 長出同名登記,開場就點名,
  不必再靠「戳記莫名其妙失效」才發現,附 8 條迴歸測試
- 兩支新閘:61→63 支、84→86 條註冊(在自己的樹上實數,不是加減推)

InkStoneCo 那邊的清理(37 支殘骸從 settings.json 與 .claude/hooks/ 移除,
kv-write-guard.sh 保留至本次升版並 /plugin update 之後)另外commit,
不在本 repo 範圍內。

驗證:重演過修好前的雙掛載(蓋一次戳記→專案版先吃掉→plugin 版恆擋,
EXIT=2),也驗過修好後單次執行放行(EXIT=0)與不蓋戳記仍被擋(EXIT=2)。

待總管定版。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 00:24:32 +08:00

112 lines
4.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <說明> <輸出> <exit碼> <該出現|!不該出現>...
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 roothooks.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 <root> <proj>
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 ]