Files
ISEP/hooks/tests/lib/hook-sandbox.sh
T
Leo f1b2ed909f 測試沙盒傳錯參數時,會把整個 tech_projects 複製進暫存區(inkstone/ISEP#122)
本輪跑迴歸測試時實撞兩次,兩次都把這台機器的磁碟寫滿:

  $ bash hooks/tests/main-and-prod-push-guard.test.sh "$PWD"
      REAL="$1" 要的是「那支 hook 的檔案路徑」,我傳了 repo 根目錄
    ⇒ hook_sandbox 不驗參數,直接 cp -R "$(dirname "$1")"
    ⇒ dirname 變成 ~/Documents/tech_projects(上一層)
    ⇒ 整個 tech_projects(所有 repo、所有 worktree)被搬進 mktemp

  實測:第一次 13 GB + 10 GB,第二次 23 GB。
        磁碟可用 25 GB → 462 MB,cp 一路吐 "No space left on device"。
        兩次都是我手動 rm -rf 才回來的(27 GB)。

🔴 而它印出來的只有一句「 沙盒建不起來」——
   沒說是參數傳錯,也沒說它已經把磁碟寫滿了。
   ⇒ 這跟本票在講的是同一句話:**閘/工具給的下一步,沒有人照著打過一次**,
     差別只在這次壞的不是逃生門,是「它壞掉時說的話」。

改法(判準是「要求某個東西在場」,不是關鍵字比對):
  ① $1 要指到一個真的檔案(空字串、目錄、不存在的路徑都不算)
  ② 它的上一層目錄名要叫 hooks(沙盒的前提就是複製一整個 hooks/)
  任一不成立 ⇒ 在 mktemp 之前 return 1,並印出走得通的那一行。

順手補上 docs/TESTING.md 之前沒寫過的一件事:**有五支測試要傳參數**,
而不傳的後果不是報錯是假綠——prod-write-guard.test.sh 的 HOOK="$1" 空掉時
每一條都執行空指令回 0 ⇒「該擋」全變成「實得 pass」,19 條假紅
(傳對參數:通過 37 / 失敗 0)。

測試:hooks/tests/hook-sandbox.test.sh 10 條(A30),通過 10 失敗 0
      ①③⑤⑥ 驗的是「收手在複製之前」,不是「訊息好不好看」
      ⑧⑨⑩ 驗正常用法沒被弄壞、複本裡沒混進 hooks/ 以外的東西
既有兩支沙盒測試複驗:
      scripts/test-main-and-prod-push-guard.sh                    13/13
      hooks/tests/main-and-prod-push-guard.test.sh          通過 10 / 失敗 0
      hooks/tests/main-and-prod-push-guard-cross-repo.test.sh 通過 19 / 失敗 0

📌 這顆跟 0.22.0 的三件事無關,是本輪路上撞到的。要拆票或丟掉這顆都行,
   它獨立於前面兩顆,而且只動 hooks/tests/(沒有任何 hook 的執行行為改變)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:54:50 +08:00

131 lines
7.5 KiB
Bash
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.
# 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 開頭會擋空值(見下)。
#
# 🔴 **先驗參數再複製**inkstone/ISEP#1222026-09-02 實撞,本檔第二次修):
# `$1` 是「那支 hook 的路徑」。傳錯(例如順手傳了 repo 根目錄)時,舊版不檢查就
# `cp -R "$(dirname "$1")" …` ⇒ **複製的是那個目錄的上一層**。
# 當天實測:`bash hooks/tests/main-and-prod-push-guard.test.sh "$PWD"`
# ⇒ dirname 變成 ~/Documents/tech_projects
# ⇒ 把 **整個 tech_projects**(所有 repo、所有 worktree)複製進 mktemp
# ⇒ 兩次就吃掉 23 GB,磁碟從 25 GB 剩到 462 MB`cp` 一路吐
# `No space left on device`,最後才印一句「❌ 沙盒建不起來」。
# ⇒ 訊息只說「建不起來」,**沒說是參數傳錯,也沒說它已經把磁碟寫滿了**。
# 判準用「要求某個東西在場」,不是關鍵字比對:
# ① `$1` 要指到一個**真的檔案**(傳目錄、傳空字串都不算)
# ② 它的上一層目錄名要叫 `hooks`(沙盒的前提就是「複製一整個 hooks/」)
# 兩條任一不成立就**在複製之前**收手,並印出走得通的那一行。
hook_sandbox() {
_hs_real=$1
if [ -z "${_hs_real:-}" ] || [ ! -f "$_hs_real" ]; then
printf '❌ hook_sandbox:第一個參數要是「那支 hook 的檔案路徑」,實得 %s\n' \
"${_hs_real:-(空的)}" >&2
printf ' 例:bash hooks/tests/main-and-prod-push-guard.test.sh hooks/main-and-prod-push-guard.sh\n' >&2
printf ' (不必自己傳的版本:bash scripts/test-main-and-prod-push-guard.sh\n' >&2
return 1
fi
_hs_hooks=$(CDPATH= cd -- "$(dirname -- "$_hs_real")" && pwd) || return 1
if [ "$(basename "$_hs_hooks")" != "hooks" ]; then
printf '❌ hook_sandbox%s 的上一層不是 hooks/,而沙盒要複製的就是那個目錄。\n' \
"$_hs_real" >&2
printf ' 算出來的來源是 %s——照複製下去會把它整個搬進暫存區(2026-09-02 這樣寫滿過磁碟)。\n' \
"$_hs_hooks" >&2
printf ' 例:bash hooks/tests/main-and-prod-push-guard.test.sh hooks/main-and-prod-push-guard.sh\n' >&2
return 1
fi
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
}