f1b2ed909f
本輪跑迴歸測試時實撞兩次,兩次都把這台機器的磁碟寫滿:
$ 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>
131 lines
7.5 KiB
Bash
131 lines
7.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 開頭會擋空值(見下)。
|
||
#
|
||
# 🔴 **先驗參數再複製**(inkstone/ISEP#122,2026-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
|
||
}
|