950c3e1919
現象(實測,不是推論):跑完 main-and-prod-push-guard 那三支測試之後
$ git status --short
M hooks/lib/__pycache__/strip_heredoc.cpython-314.pyc
M pending-main-push/unnamed--ISEP.md
?? pending-main-push/unnamed--A.md
成因:這支閘擋下推 main 的同時,會把那次請求寫成
`<hooks 的上一層>/pending-main-push/<誰>--<repo>.md`,而三支測試的測資本來
就全是推 main。於是每跑一次測試,工作區就多/改幾筆**偽造的待裁決**。
兩個後果,後者比較貴:
① `git add -A` 很容易把它們帶進 commit(08-27 那次真的帶進去了,事後才拔掉)
② 總管的迴圈讀那個目錄,讀到的每一筆都該是真的在等他裁——
測試每跑一次就偽造一筆 ⇒ **下一筆真的請求會混在雜訊裡**。
這跟「永遠在響的警報」是同一個病。
改法(產品程式碼一行都沒動):
- 新增 hooks/tests/lib/hook-sandbox.sh:把整個 hooks/ 複製到暫存區再跑複本。
閘算 pending 目錄的位置靠的是 `$0` 的上一層 ⇒ 請求寫進暫存區。
**刻意不在閘上開一個「寫去哪」的環境變數**——那種開關同時是一條把紀錄關掉的路。
- 三支測試改測沙盒複本,並各補兩條斷言:
① repo 的 pending-main-push 一個位元都沒動
② 沙盒裡**真的有**留下請求(只驗 ① 的話,把留紀錄的功能整個關掉也會綠)
- pending-main-push/ 不再進版控(它是本機狀態不是原始碼),只留一份 README 說明規約;
既有的兩筆與那顆被追蹤的 .pyc 一併 `git rm --cached`,檔案留在硬碟上不刪。
實測:
- 三支各自 10/10、19/19、13/13(原本 8/17/11,各多兩條新斷言)
- 把 `H` 改回真跡重跑 ⇒ 新斷言兩條都紅(11/13)+工作區又髒
⇒ 這兩條斷言真的抓得到它要抓的東西
- 全套 16 支測試檔跑完 0 失敗,`git status` 沒有多出任何一行
- `claude plugin validate .` ✔ Validation passed
沒有升版:這次只動測試與版控範圍,沒有任何閘的行為改變,
不需要靠新版本號送到任何人手上;請跟著總管下一個 release 一起出去。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
5.5 KiB
Bash
102 lines
5.5 KiB
Bash
# hooks/tests/lib/hook-sandbox.sh — 讓「會寫檔的閘」在測試時寫到暫存區,不寫進 repo。
|
||
# 不是獨立掛的閘(沒進 hooks.json),給 hooks/tests 與 scripts/test-* `source` 用。
|
||
#
|
||
# 🔴 為什麼需要這支(inkstone/ISEP#59 comment 4779 第 ① 條,實測重演過):
|
||
# main-and-prod-push-guard.sh 擋下「推 main」的同時,會把那次請求寫成一份
|
||
# `<hooks 的上一層>/pending-main-push/<誰>--<repo>.md`——**那份檔案的語意是
|
||
# 「有一筆推 main 正在等總管裁」**。而測試的測資本來就全是「推 main」,
|
||
# 於是每跑一次測試,工作區就多/改幾筆**偽造的待裁決**:
|
||
#
|
||
# $ git status --short # 跑完測試之後(修之前的實測)
|
||
# M pending-main-push/unnamed--ISEP.md ← 被測試覆寫(它是被追蹤的)
|
||
# ?? pending-main-push/unnamed--A.md ← 跨 repo 測試建的臨時 repo
|
||
#
|
||
# 兩個後果,後者比較貴:
|
||
# ① `git add -A` 很容易把它們帶進 commit(08-27 那次真的帶進去了,事後才拔掉)
|
||
# ② **永遠在響的警報**:總管的迴圈讀這個目錄,讀到的每一筆都該是真的在等他裁。
|
||
# 測試每跑一次就偽造一筆 ⇒ 下一筆真的請求會混在雜訊裡。
|
||
#
|
||
# 做法:把整個 `hooks/` 複製到暫存區再跑那支複本。閘算 `pending-main-push` 的位置
|
||
# 靠的是 `$0` 的上一層,所以複本會寫進暫存區,**產品程式碼一行都不用改**
|
||
# (不必為了測試在閘上多開一個「寫去哪」的開關——那種開關會變成關掉紀錄的路)。
|
||
#
|
||
# 用法:
|
||
# . "$(dirname "$0")/lib/hook-sandbox.sh"
|
||
# hook_sandbox "$REAL_HOOK" || exit 1
|
||
# G=$HOOK_SANDBOX_HOOK # 之後一律測 $G
|
||
# before=$(hook_sandbox_hostsum) # repo 那份 pending-main-push 的指紋
|
||
# ... 跑測試 ...
|
||
# hook_sandbox_assert "$before" # 回 0=乾淨且沙盒裡真的有留下請求
|
||
|
||
# hook_sandbox <真跡 hook 的路徑> → 建好沙盒,設三個變數,回 0/1
|
||
# HOOK_SANDBOX_HOOK 沙盒裡那支同名 hook(之後一律測這支)
|
||
# HOOK_SANDBOX 沙盒根(`pending-main-push` 會長在這底下)
|
||
# HOOK_SANDBOX_HOST 真跡 repo 的 pending-main-push(拿來比對有沒有被弄髒)
|
||
#
|
||
# 🔴 **刻意不用 `printf` 把路徑印出來讓呼叫端 `G=$(hook_sandbox …)` 接**——
|
||
# 命令替換跑在子殼裡,函式設的那三個變數**回不到呼叫端**。
|
||
# 寫這支的當下就這樣寫,第一次跑的結果是:`HOOK_SANDBOX` 在呼叫端是空的
|
||
# ⇒ hostsum 兩次都回 `NODIR`(相等)、沙盒的請求數當然是 0
|
||
# ⇒ **第一條斷言變成「拿空的比空的」的假綠**。
|
||
# 所以改成「設變數、不印」,而且 hook_sandbox_assert 開頭會擋空值(見下)。
|
||
hook_sandbox() {
|
||
_hs_real=$1
|
||
_hs_hooks=$(CDPATH= cd -- "$(dirname -- "$_hs_real")" && pwd) || return 1
|
||
HOOK_SANDBOX=$(mktemp -d) || return 1
|
||
cp -R "$_hs_hooks" "$HOOK_SANDBOX/hooks" || return 1
|
||
HOOK_SANDBOX_HOST="${_hs_hooks%/hooks}/pending-main-push"
|
||
HOOK_SANDBOX_HOOK="$HOOK_SANDBOX/hooks/$(basename "$_hs_real")"
|
||
[ -f "$HOOK_SANDBOX_HOOK" ] || return 1
|
||
}
|
||
|
||
# hook_sandbox_hostsum → 印出真跡 repo 那份 pending-main-push 的指紋(檔名+內容)
|
||
# 目錄不存在時印 NODIR——「本來就沒有」跟「空的」要分得出來。
|
||
hook_sandbox_hostsum() {
|
||
if [ -d "${HOOK_SANDBOX_HOST:-/nonexistent}" ]; then
|
||
find "$HOOK_SANDBOX_HOST" -type f 2>/dev/null | LC_ALL=C sort | while IFS= read -r _f; do
|
||
cksum "$_f" 2>/dev/null || printf 'UNREADABLE %s\n' "$_f"
|
||
done
|
||
else
|
||
printf 'NODIR\n'
|
||
fi
|
||
}
|
||
|
||
# hook_sandbox_assert <before 的指紋> → 印出兩行結果,全過回 0
|
||
# 🔴 兩件都要驗,只驗前者會養出「把紀錄關掉也算通過」的假綠:
|
||
# ① repo 那份 pending-main-push 一個位元都沒動(測試沒有偽造待裁決)
|
||
# ② 沙盒那份真的多了請求檔(**留請求這件事本身還在做**)
|
||
hook_sandbox_assert() {
|
||
_hs_before=$1
|
||
# fail-closed:沙盒沒建起來(或變數沒傳到這裡)時,下面兩條會變成「空的比空的」=假綠。
|
||
if [ -z "${HOOK_SANDBOX:-}" ] || [ -z "${HOOK_SANDBOX_HOST:-}" ]; then
|
||
printf ' ❌ %-58s\n' "沙盒變數是空的——這兩條斷言等於沒跑,當失敗處理"
|
||
HS_PASS=0; HS_FAIL=2; return 1
|
||
fi
|
||
_hs_after=$(hook_sandbox_hostsum)
|
||
_hs_rc=0; HS_PASS=0; HS_FAIL=0
|
||
if [ "$_hs_before" = "$_hs_after" ]; then
|
||
printf ' ✅ %-58s\n' "repo 的 pending-main-push 沒被測試碰過"; HS_PASS=$((HS_PASS+1))
|
||
else
|
||
printf ' ❌ %-58s\n' "repo 的 pending-main-push 被測試寫髒了"
|
||
printf ' before/after diff:\n'
|
||
printf '%s\n' "$_hs_before" > "$HOOK_SANDBOX/.before"
|
||
printf '%s\n' "$_hs_after" > "$HOOK_SANDBOX/.after"
|
||
diff "$HOOK_SANDBOX/.before" "$HOOK_SANDBOX/.after" | sed 's/^/ /'
|
||
_hs_rc=1; HS_FAIL=$((HS_FAIL+1))
|
||
fi
|
||
_hs_n=$(find "$HOOK_SANDBOX/pending-main-push" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||
if [ "${_hs_n:-0}" -gt 0 ]; then
|
||
printf ' ✅ %-58s(%s 筆)\n' "請求有被留下來,只是留在沙盒裡" "$_hs_n"; HS_PASS=$((HS_PASS+1))
|
||
else
|
||
printf ' ❌ %-58s\n' "沙盒裡一筆請求都沒有——留請求的機制可能被關掉了"
|
||
_hs_rc=1; HS_FAIL=$((HS_FAIL+1))
|
||
fi
|
||
return $_hs_rc
|
||
}
|
||
|
||
hook_sandbox_cleanup() {
|
||
case "${HOOK_SANDBOX:-}" in
|
||
/*/*) rm -rf "$HOOK_SANDBOX" ;; # 只刪 mktemp 給的那種深路徑,不接受空值/根目錄
|
||
esac
|
||
}
|