# 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」的同時,會把那次請求寫成一份 # `/pending-main-push/<誰>--.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 → 印出兩行結果,全過回 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 }