#!/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 G="$1" STAMP=/tmp/.main-push-ok WORK=$(mktemp -d) trap 'rm -rf "$WORK"; rm -f "$STAMP"' 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 <說明> <指令> <期望 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 "────── 通過 $pass / 失敗 $fail" [ "$fail" = 0 ]