Files
ISEP/hooks/tests/sdd-guard-retired.test.sh
T
Claude c7af690c2b sdd-guard 退役:讓「取消 Active SDD」這個裁決真的執行得下去(inkstone/ISEP#91)
leo 2026-08-16 在 inkstone/InkStoneCo#40 → comment 2942 裁定「取消 Active SDD」,
11 天沒被執行。原因不是有人偷懶,是**執行它的第一步會鎖死自己**:

    sdd-guard.sh 是 fail-closed 的
      · 動 code 檔時 status: active 不是恰好 1 份 → 擋(0 份也擋)
      · 路徑所在的 repo 沒有 3-specs           → 也擋

⇒ 照裁決把最後那份 active 拿掉 → 變 0 份 → 任何人動任何 .ts/.py/.go 全被擋。

而 ISEP 這個 repo 自己就沒有 3-specs——本票開工第一件事實測到:

    $ echo '{"tool_name":"Edit","tool_input":{"file_path":".../hooks/lib/dispatch_parse.py"}}' \
        | bash hooks/sdd-guard.sh
    🚫 SDD 協議攔截:… 找不到任何 SDD        exit 2

**它一直在誤攔 ISEP 自己,只是沒人回報。**

── 這一版做了什麼 ────────────────────────────────────
· hooks/sdd-guard.sh 刪除,hooks.json 取消註冊(85 → 84 條,只少這一條)
· commands/sdd-check.md 改寫:SDD 只記「起初的樣子」,任務本體在 Gitea 票;
  找不到 SDD 不再是停下來的理由,找不到票才是
· agents/inkstoneco-hand.md 拿掉「任何時刻只允許一份 status: active」那條紅線
· hooks/lib/path-resolve.sh 只加註解:它的唯一 caller 走了,但別順手刪
  (#22 學到的東西住在裡面,十幾支閘還在用它要修的那個寫法)

── 迴歸測試:測的不是「檔案刪了沒」,是「那個擋還會不會發生」──
hooks/tests/sdd-guard-retired.test.sh(通過 8/失敗 0,離線):
把「裁決執行完之後的世界」(沒有 3-specs 的 repo、兩份 status: active 的 repo)
丟給 hooks.json 上**整組** Write|Edit|MultiEdit 的閘——清單當場從 hooks.json 讀、
不寫死——不准有任何一支用 SDD/3-specs 當理由擋下來。
⇒ 日後有人換個檔名把同一個形狀種回來,這支照樣紅。

🔴 判準刻意不是「一支閘都不准擋」:同組還住著跟 SDD 無關、且看 session 狀態
決定擋不擋的閘(history-first/subagent-first)。把它們算失敗,這支測試會在別人
改別的東西時無故變紅,紅久了就沒人看——誤攔比漏擋更該修,對測試一樣成立。

**紅的證明**:把 sdd-guard 暫時復原(檔案+註冊)重跑 → 通過 1/失敗 7,
四條行為格全部指名 sdd-guard.sh。已還原。

── 自己跑過的 ──────────────────────────────────────
· hooks/tests/sdd-guard-retired.test.sh                通過 8/失敗 0
· 逐支點名 hooks.json(README「裝什麼」那道指令)      84 條,對 main 做集合差:
  少了 sdd-guard.sh × 1,多出 0 支,其餘一支不差
· 盤點數字全部在這棵樹上實數,不是加減推:
  ls hooks/*.sh|wc -l = 61(原 62)
  grep -c '"command":' hooks/hooks.json = 84(原 85)
  agents 7/commands 7/skills 2/scripts 48(皆未變動)
· 盤點表對帳三格(hooks-inventory 自己寫死的那三道):三格皆無輸出
· scripts/check-version-consistency.sh                  0.16.2 一致
· claude plugin validate .                              Validation passed
· hooks/tests/ 全部離線測試 24 支重跑:本次改動 0 退步
  (dispatch-format-guard 40/52、prod-write-guard 18/19-fail、
   stage-before-prod-guard 9/7-fail、另 3 支需帶參數/建不起沙盒——
   **六支在 gitea/main 上逐支重跑結果一模一樣,是既有狀態不是本次造成**)

── 沒做、也不該由這張票做的 ─────────────────────────
· inkstone/InkStoneCo 那半(拿掉 active SDD + 刪 CLAUDE.md「單一活性鐵律」段)
  ——那是 inkstoneco-hand 的 repo;而且順序上本來就要等這一版出去、
  兩邊 /plugin update 之後才動得,先拿掉就鎖死
· checkbox 分診(掛 inkstone/InkStoneCo#49,票上明寫不要另開票)
· 版本號:待總管定版

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
2026-08-31 00:56:37 +00:00

155 lines
7.2 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
# sdd-guard 退役的迴歸測試(inkstone/ISEP#91
#
# ── 這支在守什麼 ────────────────────────────────────────────────────
# leo 2026-08-16 在 inkstone/InkStoneCo#40 裁定「**取消 Active SDD**」:
# 任務狀態搬到 Gitea 管,SDD 只記「起初的樣子」。
# 但 `sdd-guard.sh` 是 **fail-closed** 的——
# 動 code 檔時,active SDD 不是「恰好 1 份」→ 擋(0 份也擋)
# 路徑所在的 repo 沒有 3-specs → 也擋
# ⇒ 照裁決把 active SDD 拿掉,會把「動任何 .ts/.py/.go」整個鎖死。
# 這就是那個裁決 11 天沒人敢執行的真正原因。
#
# 所以退役的順序是「**先讓閘退役,再拿掉 active SDD**」,而這支測的是第一步做完了沒有。
#
# 🔴 它測的不是「檔案刪掉了沒有」,而是**那個擋還會不會發生**:
# 把整組 `Write|Edit|MultiEdit` 的 PreToolUse 閘,拿去撞「裁決執行完之後的世界」
# (一個沒有 3-specs 的 repo、以及一個有兩份 active SDD 的 repo),一支都不准擋。
# ⇒ 有人日後用別的檔名把同一個形狀種回來,這支照樣會紅。
#
# 用法:hooks/tests/sdd-guard-retired.test.sh
# 🔴 全程在 TMP 底下建假 repo,跑完自己清;不動任何真 repo、不打網路。
set -u
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_ROOT="$(cd "$HOOKS_DIR/.." && pwd)"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
bad() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
# ── ① 閘本體與它的註冊都不在了 ────────────────────────────────────
echo "── ① 退役:檔案與註冊 ──"
if [ -e "$HOOKS_DIR/sdd-guard.sh" ]; then
bad "hooks/sdd-guard.sh 還在(應已刪除)"
else
ok "hooks/sdd-guard.sh 已刪除"
fi
if grep -q 'sdd-guard' "$HOOKS_DIR/hooks.json"; then
bad "hooks.json 還註冊著 sdd-guard(照裁決做會鎖死所有 code 寫入)"
else
ok "hooks.json 已無 sdd-guard 註冊"
fi
# hooks.json 要仍然是合法 JSON,而且別的閘一支都沒被順手弄掉
# (README:合錯的時候「語法合法、閘卻不見了」,不會有任何東西喊一聲)
REGISTERED=$(python3 - "$HOOKS_DIR/hooks.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
print(sum(len(g.get("hooks", [])) for ev in d["hooks"].values() for g in ev))
PY
) || REGISTERED=""
if [ -n "$REGISTERED" ]; then
ok "hooks.json 仍是合法 JSON(註冊 $REGISTERED 條)"
else
bad "hooks.json 解析失敗"
fi
# ── ② 真正的驗收:裁決執行完之後,動 code 檔不能被擋 ──────────────
echo "── ② 行為:把「拿掉 active SDD 之後的世界」丟給整組寫檔閘,一支都不准擋 ──"
# 這兩個假 repo 就是舊 sdd-guard 一定會擋下的兩種狀態:
# A. 完全沒有 3-specs(=裁決執行完的樣子,也是 ISEP 自己現在的樣子)
# B. 兩份 status: active(=舊的「單一活性鐵律」違反)
REPO_NO_SDD="$TMP/repo-no-sdd"
mkdir -p "$REPO_NO_SDD/src"
git init -q "$REPO_NO_SDD"
REPO_MULTI="$TMP/repo-multi-active"
mkdir -p "$REPO_MULTI/system-dev/docs/3-specs/a" "$REPO_MULTI/system-dev/docs/3-specs/b" "$REPO_MULTI/src"
git init -q "$REPO_MULTI"
printf -- '---\nstatus: active\n---\n# A\n' > "$REPO_MULTI/system-dev/docs/3-specs/a/design.md"
printf -- '---\nstatus: active\n---\n# B\n' > "$REPO_MULTI/system-dev/docs/3-specs/b/design.md"
# 撈出所有掛在 Write|Edit|MultiEdit 上的 PreToolUse 閘(照 hooks.json 的實況,不寫死清單)
mapfile -t WRITE_HOOKS < <(python3 - "$HOOKS_DIR/hooks.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
for g in d["hooks"].get("PreToolUse", []):
m = g.get("matcher", "") or ""
if "Write" in m or "Edit" in m:
for h in g.get("hooks", []):
print(h["command"].split("/")[-1])
PY
)
if [ "${#WRITE_HOOKS[@]}" -eq 0 ]; then
bad "撈不到任何 Write|Edit 的 PreToolUse 閘——測試本身失效了,先修這裡"
fi
# 判準:擋下來的理由**是不是 SDD**。
#
# 🔴 這裡刻意不寫成「一支閘都不准擋」——同一組 matcher 上還住著好幾支跟 SDD 無關、
# 而且看 session 狀態決定擋不擋的閘(`subagent-first-guard``history-first-guard`…)。
# 把它們算成失敗,這支測試就會在別人改別的東西時無故變紅,
# 紅久了就沒人看——`docs/TESTING.md` 開頭那句「誤攔比漏擋更該修」對測試一樣成立。
# 所以失敗的定義是:**有閘擋下來,而且它擋的理由指向 SDD/3-specs**。
# 其餘的擋只印出來給人看,不判失敗。
probe() { # probe <說明> <file_path>
local desc="$1" path="$2" sdd_blocked="" other_blocked=""
local payload out rc
payload=$(python3 -c "import json,sys;print(json.dumps({'tool_name':'Write','tool_input':{'file_path':sys.argv[1],'content':'export const x = 1;\n'}}))" "$path")
for name in "${WRITE_HOOKS[@]}"; do
[ -x "$HOOKS_DIR/$name" ] || continue
out=$(printf '%s' "$payload" | "$HOOKS_DIR/$name" 2>&1)
rc=$?
[ "$rc" -eq 2 ] || continue
if printf '%s' "$out" | grep -qE 'SDD|3-specs'; then
sdd_blocked="$sdd_blocked $name"
else
other_blocked="$other_blocked $name"
fi
done
if [ -n "$sdd_blocked" ]; then
bad "$desc —— 仍被 SDD 理由擋下:$sdd_blocked"
else
ok "$desc"
[ -n "$other_blocked" ] && echo " (另有與 SDD 無關的閘擋下,不算失敗:$other_blocked"
fi
}
# 刻意讓 $_rootCLAUDE_PROJECT_DIRpwd)跟這些假 repo 對不上——
# 舊 sdd-guard 在這個情境下走的正是 fail-closed 那條分支。
unset CLAUDE_PROJECT_DIR
cd "$TMP" || exit 1
probe "repo 沒有 3-specs(裁決執行完的樣子)→ 寫 .ts 不被擋" "$REPO_NO_SDD/src/x.ts"
probe "repo 沒有 3-specs → 寫 .py 不被擋" "$REPO_NO_SDD/src/x.py"
probe "兩份 status: active(舊「單一活性」違反)→ 寫 .ts 不被擋" "$REPO_MULTI/src/x.ts"
# ISEP 自己就是「沒有 3-specs 的 repo」——退役前,改自己的 .py 當場被擋。
probe "ISEP 自己的 hooks/lib/*.py → 不被擋(退役前這一格是紅的)" "$REPO_ROOT/hooks/lib/dispatch_parse.py"
# ── ③ 會被載入的那幾份,不能再有人讀到「只准一份 active SDD」 ──────
echo "── ③ 條文:載入面不再宣告單一活性鐵律 ──"
# 只查**會被載入**的那幾份(commands / agents / skills / hooks 腳本)。
# docs/ 與 wiki/ 不查——它們得說得出「退役了什麼」,不然這件事沒有歷史。
STALE=""
while IFS= read -r f; do
grep -qE '只(准|允許)一份|單一活性' "$f" && STALE="$STALE $f"
done < <(find "$REPO_ROOT/commands" "$REPO_ROOT/agents" "$REPO_ROOT/skills" -name '*.md' 2>/dev/null
find "$REPO_ROOT/hooks" -maxdepth 1 -name '*.sh' 2>/dev/null)
if [ -n "$STALE" ]; then
bad "這些載入面還在宣告「只准一份 active SDD」:$STALE"
else
ok "commandsagentsskillshooks 都不再宣告單一活性鐵律"
fi
echo
echo "結果:通過 $PASS 失敗 $FAIL"
[ "$FAIL" -eq 0 ] || exit 1