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>
63 lines
3.1 KiB
Bash
Executable File
63 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# kv-write-guard.sh 的迴歸測試(inkstone/ISEP#122 搬進 ISEP 時補上——
|
|
# 原檔 2026-08-25 立於 InkStoneCo 專案版,搬過來之前沒有自動化測試)。
|
|
set -u
|
|
HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/kv-write-guard.sh}"
|
|
PASS=0; FAIL=0; N=0
|
|
|
|
# run <tool> <file_path> <content> → 送一個 Write 事件的 payload
|
|
run() {
|
|
python3 -c '
|
|
import json,sys
|
|
tool, fp, content = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
print(json.dumps({"tool_name": tool, "tool_input": {"file_path": fp, "content": content}}))
|
|
' "$1" "$2" "$3" | bash "$HOOK" 2>&1
|
|
echo "EXIT:$?"
|
|
}
|
|
|
|
check() { # check <說明> <輸出+EXIT行> <該擋(2)|該放行(0)> <該出現|!不該出現>...
|
|
desc="$1"; out="$2"; want="$3"; shift 3
|
|
code="$(printf '%s\n' "$out" | grep -oE 'EXIT:[0-9]+' | tail -1 | cut -d: -f2)"
|
|
N=$((N+1)); ok=1; why=""
|
|
if [ "$code" != "$want" ]; then ok=0; why="要 exit $want,實際 $code"; fi
|
|
for w in "$@"; do
|
|
case "$w" in
|
|
"!"*) if printf '%s' "$out" | grep -qF -- "${w#!}"; then ok=0; why="$why 不該出現卻出現:${w#!}"; fi ;;
|
|
*) if ! printf '%s' "$out" | grep -qF -- "$w"; then ok=0; why="$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
|
|
}
|
|
|
|
echo "── 該擋:往 KV binding 寫入 ──────────────────────────────────"
|
|
out=$(run "Write" "src/app-system.ts" $'export async function save() {\n await MY_KV.put("k", "v");\n}')
|
|
check "① 全大寫 binding.put( → 擋" "$out" 2 "長效資料不准寫進 KV" "MY_KV"
|
|
|
|
out=$(run "Edit" "src/app-system.ts" $'const x = 1;\nSESSION_KV.delete(id);')
|
|
check "② .delete( 一樣擋" "$out" 2 "長效資料不准寫進 KV"
|
|
|
|
echo "── 不該擋(誤攔比漏擋嚴重)───────────────────────────────────"
|
|
|
|
out=$(run "Write" "src/app-system.ts" 'const v = await MY_KV.get("k");')
|
|
check "③ 只是讀(.get)→ 放行" "$out" 0 "!長效資料不准寫進 KV"
|
|
|
|
out=$(run "Write" "src/app-system.ts" 'MY_KV.put("k", v); // kv-ok: session nonce,有 TTL')
|
|
check "④ 加了 kv-ok 豁免留痕的那行 → 放行" "$out" 0 "!長效資料不准寫進 KV"
|
|
|
|
out=$(run "Write" "README.md" 'MY_KV.put("k", "v")')
|
|
check "⑤ 非程式碼檔(.md)→ 放行" "$out" 0 "!長效資料不准寫進 KV"
|
|
|
|
out=$(run "Write" "src/kv-write-guard.test.ts" 'MY_KV.put("k", "v")')
|
|
check "⑥ 閘自己的測試檔 → 放行" "$out" 0 "!長效資料不准寫進 KV"
|
|
|
|
out=$(run "Write" "src/app-system.ts" 'const config = { retries: 3 };')
|
|
check "⑦ 完全不提 KV 的一般程式碼 → 放行" "$out" 0 "!長效資料不准寫進 KV"
|
|
|
|
out=$(run "Write" "src/app-system.ts" 'this.myKv.put("k", "v"); // 小寫 binding,不是本閘要的形狀')
|
|
check "⑧ 小寫變數名(不是「全大寫識別字」形狀)→ 放行" "$out" 0 "!長效資料不准寫進 KV"
|
|
|
|
echo ""
|
|
echo "── 結果:$PASS 通過 / $FAIL 失敗(共 $N)──"
|
|
[ "$FAIL" -eq 0 ]
|