ee085c1854
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>
112 lines
4.9 KiB
Bash
Executable File
112 lines
4.9 KiB
Bash
Executable File
#!/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 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 <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 ]
|