diff --git a/docs/TESTING.md b/docs/TESTING.md index 1c1c5d3..2ab33b2 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -379,6 +379,46 @@ bash hooks/tests/history-first-guard.test.sh - ④ 紅 ⇒ 逃生門又是假的(這是本支唯一分辨得出新舊版的那一格,見檔頭) - ⑦ 紅 ⇒ 逃生門變成永久後門(一次 `touch` 就免疫,過期不算數這件事沒了) - ⑧⑨⑩ 任一紅 ⇒ **誤攔**:文件/新檔/測試檔本來就不該被這支碰 + +### A30 — 測試沙盒傳錯參數時要在**複製之前**收手:10 條 +``` +bash hooks/tests/hook-sandbox.test.sh +``` +**該看到**:`通過 10 條,失敗 0 條`。全離線。 + +**它在守什麼**(`inkstone/ISEP#122`,2026-09-02 實撞,**兩次**): +`hooks/tests/lib/hook-sandbox.sh` 的 `hook_sandbox ` 舊版**不驗參數**, +直接 `cp -R "$(dirname "$1")"`。順手把 repo 根目錄傳進去時: + +``` +$ bash hooks/tests/main-and-prod-push-guard.test.sh "$PWD" + dirname → ~/Documents/tech_projects ← 上一層,不是 hooks/ + cp -R → 整個 tech_projects(所有 repo、所有 worktree)搬進 mktemp + 實測 一次 13 GB+一次 10 GB,磁碟可用從 25 GB 掉到 462 MB + 印出來的 ❌ 沙盒建不起來 ← 只有這一句 +``` + +🔴 **它沒說參數傳錯,也沒說它已經把磁碟寫滿了。** +判準用「要求某個東西在場」(`$1` 要是真的檔案;上一層要叫 `hooks`), +不是關鍵字比對——兩條任一不成立就在 `mktemp` 之前 `return 1`。 + +📌 **正確用法**(兩支需要傳參數的測試,`docs/TESTING.md` 之前沒寫過): +``` +bash hooks/tests/main-and-prod-push-guard.test.sh hooks/main-and-prod-push-guard.sh +bash hooks/tests/main-and-prod-push-guard-cross-repo.test.sh hooks/main-and-prod-push-guard.sh +bash hooks/tests/prod-write-guard.test.sh hooks/prod-write-guard.sh +bash hooks/tests/stage-before-prod-guard.test.sh hooks/stage-before-prod-guard.sh +bash hooks/tests/gitea-arm-check.test.sh . +``` +**不傳的後果不是報錯,是假綠**:`prod-write-guard.test.sh` 的 `HOOK="$1"` 空掉時 +每一條都執行空指令回 0 ⇒ 「該擋」全部變成「實得 pass」,**19 條假紅** +(同一支傳對參數是 `通過 37 / 失敗 0`)。不必自己傳參數的包裝在 +`scripts/test-main-and-prod-push-guard.sh`。 + +**失敗**: +- ①③⑤⑥ 任一紅 ⇒ 收手收得太晚,暫存區已經開始長東西(磁碟風險回來了) +- ② 紅 ⇒ 訊息沒給出走得通的那一行(本票整張票在講的就是這件事) +- ⑧⑨⑩ 任一紅 ⇒ **誤攔/複製錯**:正常用法被弄壞,或複本裡混進了 `hooks/` 以外的東西 ### A19 — 下游做完時頂層跟著關:49 條 ``` bash scripts/test-ticket-handoff-writeback.sh diff --git a/hooks/tests/hook-sandbox.test.sh b/hooks/tests/hook-sandbox.test.sh new file mode 100755 index 0000000..76ebeb0 --- /dev/null +++ b/hooks/tests/hook-sandbox.test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# hooks/tests/lib/hook-sandbox.sh 的迴歸測試(inkstone/ISEP#122) +# +# 🔴 為什麼有這支(2026-09-02 實撞,證據在票上): +# `hook_sandbox` 的第一個參數是「那支 hook 的檔案路徑」,傳錯時舊版不檢查就 +# `cp -R "$(dirname "$1")"` ⇒ 複製的是上一層。傳 repo 根目錄進去, +# 複製的就是**整個 tech_projects**:兩次吃掉 23 GB,磁碟剩 462 MB, +# 而它只印一句「沙盒建不起來」——**沒說參數錯,也沒說它已經把磁碟寫滿**。 +# +# 所以這支測的不是「錯誤訊息好不好看」,是**收手的時機**: +# ④⑤ 兩條驗「複製之前就 return 1」——暫存區裡不准留下任何東西。 +# +# 用法:bash hooks/tests/hook-sandbox.test.sh +set -u +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT=$(CDPATH= cd -- "$HERE/.." && pwd) # …/hooks +REPO=$(CDPATH= cd -- "$ROOT/.." && pwd) +PASS=0; FAIL=0 +ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); } +no(){ printf ' ❌ %s —— %s\n' "$1" "$2"; FAIL=$((FAIL+1)); } + +# 每一條都在自己的子殼裡跑:hook_sandbox 會設全域變數,不隔離會互相污染。 +run(){ # run <參數> → 印 "rc|沙盒路徑|訊息" + ( . "$HERE/lib/hook-sandbox.sh" + msg=$(hook_sandbox "$1" 2>&1); rc=$? + printf '%s|%s|%s' "$rc" "${HOOK_SANDBOX:-}" "$(printf '%s' "$msg" | tr '\n' ' ')" + ) +} + +echo "── 該收手(傳錯參數)──────────────────────────────────────" + +r=$(run "$REPO"); rc=${r%%|*}; rest=${r#*|}; sb=${rest%%|*}; msg=${rest#*|} +[ "$rc" = "1" ] && ok "① 傳 repo 根目錄(本票的實撞)→ return 1" \ + || no "① 傳 repo 根目錄(本票的實撞)→ return 1" "實得 rc=$rc" +case "$msg" in *"hooks/main-and-prod-push-guard.sh"*) + ok "② 訊息給得出一行**真的跑得起來**的用法" ;; + *) no "② 訊息給得出一行**真的跑得起來**的用法" "訊息:$msg" ;; +esac +if [ -z "$sb" ] || [ ! -d "$sb" ]; then ok "③ 收手在 mktemp 之前,暫存區沒有殘骸" +else no "③ 收手在 mktemp 之前,暫存區沒有殘骸" "留下了 $sb"; fi + +r=$(run ""); [ "${r%%|*}" = "1" ] && ok "④ 空參數 → return 1(不是拿空字串去 dirname)" \ + || no "④ 空參數 → return 1" "實得 rc=${r%%|*}" + +r=$(run "$REPO/scripts/ticket"); rc=${r%%|*}; rest=${r#*|}; sb=${rest%%|*} +if [ "$rc" = "1" ]; then ok "⑤ 檔案存在但上一層不是 hooks/ → 一樣收手" +else no "⑤ 檔案存在但上一層不是 hooks/ → 一樣收手" "實得 rc=$rc"; fi +if [ -z "$sb" ] || [ ! -d "$sb" ]; then ok "⑥ ⑤ 這條也沒把 scripts/ 複製出去" +else no "⑥ ⑤ 這條也沒把 scripts/ 複製出去" "留下了 $sb"; fi + +r=$(run "$ROOT/沒有這支.sh"); [ "${r%%|*}" = "1" ] && ok "⑦ 路徑不存在 → return 1" \ + || no "⑦ 路徑不存在 → return 1" "實得 rc=${r%%|*}" + +echo "── 不准把本來會過的弄壞(誤攔比漏擋嚴重)────────────────────" +( . "$HERE/lib/hook-sandbox.sh" + hook_sandbox "$ROOT/main-and-prod-push-guard.sh"; rc=$? + if [ "$rc" != "0" ]; then echo "RC=$rc"; exit 0; fi + [ -f "$HOOK_SANDBOX_HOOK" ] && echo "HOOKOK" + [ -d "$HOOK_SANDBOX/hooks/lib" ] && echo "LIBOK" + # 只複製 hooks/,不該把 repo 的其他目錄帶進來 + [ -d "$HOOK_SANDBOX/hooks/scripts" ] && echo "LEAK" + hook_sandbox_cleanup +) > /tmp/.hs-ok.$$ 2>&1 +grep -q HOOKOK /tmp/.hs-ok.$$ && ok "⑧ 正常用法照樣建得起來,複本裡有那支 hook" \ + || no "⑧ 正常用法照樣建得起來" "$(cat /tmp/.hs-ok.$$)" +grep -q LIBOK /tmp/.hs-ok.$$ && ok "⑨ 整個 hooks/(含 lib/)都在複本裡" \ + || no "⑨ 整個 hooks/(含 lib/)都在複本裡" "$(cat /tmp/.hs-ok.$$)" +grep -q LEAK /tmp/.hs-ok.$$ && no "⑩ 只複製 hooks/,沒有把 repo 其他目錄帶進去" "複本裡出現 scripts/" \ + || ok "⑩ 只複製 hooks/,沒有把 repo 其他目錄帶進去" +rm -f /tmp/.hs-ok.$$ + +echo +echo "通過 $PASS 條,失敗 $FAIL 條(共 $((PASS+FAIL)) 條)" +[ "$FAIL" -eq 0 ] diff --git a/hooks/tests/lib/hook-sandbox.sh b/hooks/tests/lib/hook-sandbox.sh index 57f62f1..9d62968 100644 --- a/hooks/tests/lib/hook-sandbox.sh +++ b/hooks/tests/lib/hook-sandbox.sh @@ -39,9 +39,38 @@ # ⇒ 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"