Files
ISEP/hooks/tests/main-and-prod-push-guard-cross-repo.test.sh
Leo 950c3e1919 測試不再偽造「有一筆推 main 在等總管裁」(inkstone/ISEP#59)
現象(實測,不是推論):跑完 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>
2026-08-27 18:33:44 +08:00

121 lines
5.8 KiB
Bash
Executable File
Raw Permalink 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.
#!/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 "── 同 reposession 站著的那個)舊行為原封不動 ──"
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 ]