From f1b2ed909fdb65aef581715debcb85e3a8b81223 Mon Sep 17 00:00:00 2001 From: richblack Date: Wed, 2 Sep 2026 11:54:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=AC=E8=A9=A6=E6=B2=99=E7=9B=92=E5=82=B3?= =?UTF-8?q?=E9=8C=AF=E5=8F=83=E6=95=B8=E6=99=82=EF=BC=8C=E6=9C=83=E6=8A=8A?= =?UTF-8?q?=E6=95=B4=E5=80=8B=20tech=5Fprojects=20=E8=A4=87=E8=A3=BD?= =?UTF-8?q?=E9=80=B2=E6=9A=AB=E5=AD=98=E5=8D=80=EF=BC=88inkstone/ISEP#122?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本輪跑迴歸測試時實撞兩次,兩次都把這台機器的磁碟寫滿: $ 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 --- docs/TESTING.md | 40 +++++++++++++++++ hooks/tests/hook-sandbox.test.sh | 74 ++++++++++++++++++++++++++++++++ hooks/tests/lib/hook-sandbox.sh | 29 +++++++++++++ 3 files changed, 143 insertions(+) create mode 100755 hooks/tests/hook-sandbox.test.sh 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"