950c3e1919
現象(實測,不是推論):跑完 main-and-prod-push-guard 那三支測試之後
$ git status --short
M hooks/lib/__pycache__/strip_heredoc.cpython-314.pyc
M pending-main-push/unnamed--ISEP.md
?? pending-main-push/unnamed--A.md
成因:這支閘擋下推 main 的同時,會把那次請求寫成
`<hooks 的上一層>/pending-main-push/<誰>--<repo>.md`,而三支測試的測資本來
就全是推 main。於是每跑一次測試,工作區就多/改幾筆**偽造的待裁決**。
兩個後果,後者比較貴:
① `git add -A` 很容易把它們帶進 commit(08-27 那次真的帶進去了,事後才拔掉)
② 總管的迴圈讀那個目錄,讀到的每一筆都該是真的在等他裁——
測試每跑一次就偽造一筆 ⇒ **下一筆真的請求會混在雜訊裡**。
這跟「永遠在響的警報」是同一個病。
改法(產品程式碼一行都沒動):
- 新增 hooks/tests/lib/hook-sandbox.sh:把整個 hooks/ 複製到暫存區再跑複本。
閘算 pending 目錄的位置靠的是 `$0` 的上一層 ⇒ 請求寫進暫存區。
**刻意不在閘上開一個「寫去哪」的環境變數**——那種開關同時是一條把紀錄關掉的路。
- 三支測試改測沙盒複本,並各補兩條斷言:
① repo 的 pending-main-push 一個位元都沒動
② 沙盒裡**真的有**留下請求(只驗 ① 的話,把留紀錄的功能整個關掉也會綠)
- pending-main-push/ 不再進版控(它是本機狀態不是原始碼),只留一份 README 說明規約;
既有的兩筆與那顆被追蹤的 .pyc 一併 `git rm --cached`,檔案留在硬碟上不刪。
實測:
- 三支各自 10/10、19/19、13/13(原本 8/17/11,各多兩條新斷言)
- 把 `H` 改回真跡重跑 ⇒ 新斷言兩條都紅(11/13)+工作區又髒
⇒ 這兩條斷言真的抓得到它要抓的東西
- 全套 16 支測試檔跑完 0 失敗,`git status` 沒有多出任何一行
- `claude plugin validate .` ✔ Validation passed
沒有升版:這次只動測試與版控範圍,沒有任何閘的行為改變,
不需要靠新版本號送到任何人手上;請跟著總管下一個 release 一起出去。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
5.8 KiB
Bash
Executable File
121 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 跨 repo 戳記實測(inkstone/ISEP#30 comment 3949,脈絡 inkstone/InkStoneCo#57)
|
||
#
|
||
# 補的是什麼:hooks/tests/main-and-prod-push-guard.test.sh 那八向都只在單一 repo
|
||
# (測試腳本自己所在的 repo)裡驗證,從沒測過「站在 A、要推 B 的 main」這個形狀
|
||
# ——而這正是 2026-08-21 真的撞到、讓戳記永遠對不上的那個情境。這支專門補這塊。
|
||
#
|
||
# 用法:main-and-prod-push-guard-cross-repo.test.sh <要測的 hook 絕對路徑>
|
||
# 路徑務必給絕對路徑——測試會 cd 進臨時建立的 A/B repo 再呼叫它,相對路徑
|
||
# 到那時就對不上了(自己撞過一次:exit=127 command not found)。
|
||
set -u
|
||
REAL="$1"
|
||
|
||
# 🔴 測沙盒裡的複本,不測真跡:這支閘擋下推 main 時會把請求寫進
|
||
# `<repo>/pending-main-push/`,而這裡每一條測資都是推 main
|
||
# ⇒ 直接測真跡,每跑一次就在 ISEP 的工作區偽造幾筆「還沒裁」
|
||
# (實測留下過 `pending-main-push/unnamed--A.md`——A 是這支自己建的臨時 repo,
|
||
# inkstone/ISEP#59 comment 4779 第 ① 條)。收尾會驗兩件(見 lib 檔頭)。
|
||
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib/hook-sandbox.sh"
|
||
hook_sandbox "$REAL" || { echo "❌ 沙盒建不起來"; exit 1; }
|
||
G=$HOOK_SANDBOX_HOOK
|
||
HOSTSUM_BEFORE=$(hook_sandbox_hostsum)
|
||
|
||
STAMP=/tmp/.main-push-ok
|
||
WORK=$(mktemp -d)
|
||
trap 'rm -rf "$WORK"; rm -f "$STAMP"; hook_sandbox_cleanup' EXIT
|
||
|
||
for d in A B; do
|
||
git init -q -b main "$WORK/$d"
|
||
git -C "$WORK/$d" config user.email t@t.com
|
||
git -C "$WORK/$d" config user.name t
|
||
echo x > "$WORK/$d/f.txt"
|
||
git -C "$WORK/$d" add f.txt
|
||
git -C "$WORK/$d" commit -q -m init
|
||
done
|
||
A="$WORK/A"; B="$WORK/B"
|
||
|
||
pass=0; fail=0
|
||
t() { # t <說明> <cwd> <指令> <期望 exit>
|
||
local desc="$1" cwd="$2" cmd="$3" want="$4"
|
||
local rc
|
||
rc=$(cd "$cwd" && CLAUDE_CODE_CHILD_SESSION=1 python3 -c '
|
||
import json, subprocess, sys
|
||
p = subprocess.run(["bash", sys.argv[2]],
|
||
input=json.dumps({"tool_name": "Bash",
|
||
"tool_input": {"command": sys.argv[1]}}),
|
||
capture_output=True, text=True)
|
||
print(p.returncode)
|
||
' "$cmd" "$G")
|
||
if [ "$rc" = "$want" ]; then printf ' ✅ %-58s exit=%s\n' "$desc" "$rc"; pass=$((pass+1))
|
||
else printf ' ❌ %-58s exit=%s(期望 %s)\n' "$desc" "$rc" "$want"; fail=$((fail+1)); fi
|
||
}
|
||
|
||
echo "── 2026-08-21 實撞的原形狀:站在 A,要推 B 的 main ──"
|
||
rm -f "$STAMP"
|
||
t "沒戳記 → 擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||
t "替 B 開的戳記 → 推 B 的 main 該放行(舊版在此情境永遠擋,這是本票要修的洞)" \
|
||
"$A" "cd $B && git push origin HEAD:main" 0
|
||
|
||
echo "── 反向不准鬆:替 A 開的戳記,不能拿去放行推 B(08-11 那次穿透的形狀)──"
|
||
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||
t "替 A 開的戳記 → 拿去推 B 的 main 必須仍被擋" \
|
||
"$A" "cd $B && git push origin HEAD:main" 2
|
||
rm -f "$STAMP"
|
||
|
||
echo "── git -C 語法要吃到同一套判斷 ──"
|
||
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||
t "替 B 開戳記,用 git -C B push" "$A" "git -C $B push origin main" 0
|
||
rm -f "$STAMP"
|
||
|
||
echo "── 08-11 原始穿透的形狀:子殼裡的 cd 不能外洩到殼外 ──"
|
||
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||
t "子殼裡 cd 去 B 但沒在殼內推;殼外站著 A 真的推 → 符合 A 的戳記,放行" \
|
||
"$A" "(cd $B && true); git push origin HEAD:main" 0
|
||
rm -f "$STAMP"
|
||
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||
t "子殼裡 cd 去 B 且在殼內真的推 → 目標是 B,戳記是 A,必須擋" \
|
||
"$A" "(cd $B && git push origin HEAD:main)" 2
|
||
rm -f "$STAMP"
|
||
|
||
echo "── 順手抓到、一併修的洞:純括號包住整條指令,不准繞過目的地判斷 ──"
|
||
t "(git push origin HEAD:main) 沒有任何戳記 → 必須擋(舊版在此整段放行)" \
|
||
"$A" "(git push origin HEAD:main)" 2
|
||
|
||
echo "── 同 repo(session 站著的那個)舊行為原封不動 ──"
|
||
rm -f "$STAMP"
|
||
t "站在 A 推 A 自己的 main,沒戳記 → 擋" "$A" "git push origin HEAD:main" 2
|
||
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||
t "站在 A 推 A 自己的 main,替 A 開戳記 → 放行" "$A" "git push origin HEAD:main" 0
|
||
rm -f "$STAMP"
|
||
|
||
echo "── 舊有行為一條都不能壞 ──"
|
||
t "推 feature branch 放行" "$A" "git push origin feat/xyz" 0
|
||
t "推 tag 放行" "$A" "git push origin refs/tags/v1.0.0" 0
|
||
t "只是提到 main 的 gh pr create,放行" "$A" "gh pr create --base main --title t" 0
|
||
|
||
echo "── subagent 沒戳記,即使 cd 去別的 repo 也照擋 ──"
|
||
rm -f "$STAMP"
|
||
t "subagent 站在 A、cd 去 B 推 main,沒戳記仍擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||
|
||
echo "── 單次用完即丟、900 秒逾時:換到跨 repo 場景一樣要成立 ──"
|
||
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||
t "第一次:替 B 開戳記推 B → 放行" "$A" "cd $B && git push origin HEAD:main" 0
|
||
t "第二次:同一枚戳記(已用掉)再推一次 → 應該擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||
rm -f "$STAMP"; touch "$STAMP"
|
||
t "touch 出的空戳記 → 推 B 的 main 仍應擋(08-12 補的洞不能被本次改動重開)" \
|
||
"$A" "cd $B && git push origin HEAD:main" 2
|
||
rm -f "$STAMP"
|
||
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||
touch -t "$(date -v-16M +%Y%m%d%H%M.%S 2>/dev/null || date -d '-16 minutes' +%Y%m%d%H%M.%S)" "$STAMP" 2>/dev/null
|
||
t "16 分鐘前開的戳記 → 已過期,推 B 應擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||
rm -f "$STAMP"
|
||
|
||
echo "── 測試自己不准弄髒工作區(inkstone/ISEP#59)──"
|
||
hook_sandbox_assert "$HOSTSUM_BEFORE"
|
||
pass=$((pass+HS_PASS)); fail=$((fail+HS_FAIL))
|
||
|
||
echo "────── 通過 $pass / 失敗 $fail"
|
||
[ "$fail" = 0 ]
|