3e83a4b4f3
三支閘(prod-write-guard/main-and-prod-push-guard/stage-before-prod-guard)
判斷戳記新不新,都寫成這一行:
MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
macOS(BSD stat)上 `-f %m` 就是 mtime,對的。
**GNU coreutils 的 `-f` 是「顯示檔案系統資訊」**,而且它一邊回非零、
一邊往 stdout 吐一整段區塊 ⇒ `||` 接上的秒數被那段文字污染
⇒ 下一行的 `case "$NOW$MT" in *[!0-9]*) return 1` 必然命中
⇒ **在 Linux 上那三支閘的戳記永遠不會被接受。**
後果不是少一個便利功能:那三支閘都是「擋下來、但總管看過就能放行」,
而放行那道門在雲端打不開 ⇒ **它們在雲端等於純擋**,
總管照著閘自己印的指示做,做幾次都打不開,閘也不會告訴他門是壞的
(inkstone/InkStoneCo#99 那次「連續四次蓋不出戳記」就是這個形狀)。
· hooks/lib/mtime.sh:先 `-c %Y`(GNU)再 `-f %m`(BSD),**每一步都驗是不是純數字**
——這個 bug 的成因正是「命令失敗了卻還是印了東西」,只看離開碼會再被騙一次。
四支用到的檔各自帶一份 inline fallback:這道門不能因為少一個檔案就再關上一次。
· scripts/gate-ok:把散在各閘訊息裡的七種逃生口收斂成一個名字、一種形狀
(`gate-ok <閘名> [參數]`)。原因是逃生口原本是「臨時湊出來的 Bash 指令」,
而臨時湊的指令沒有穩定形狀可以事先放行——settings.json 的 allow 只能逐條完全比對,
多一個 `&&`、換一個 session id 就落在規則外。它蓋的戳記與閘原本認的完全同一個檔、
同一種語意(單次、綁 repo、綁 session、有效期都沒動),換掉的只有「怎麼蓋」。
測試 hooks/tests/gate-ok.test.sh:16/16。既有的三支測試(29/16/10)不受影響。
拿 main 跑同一份:②⑤(門打得開)紅、⑥⑦⑧(門沒變寬)全綠
⇒ 這支補的正是既有測試從來沒驗過的那半邊。
83 lines
5.1 KiB
Bash
Executable File
83 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# scripts/gate-ok +「戳記真的打得開門」的迴歸測試(inkstone/ISEP#90 ④)
|
||
#
|
||
# ══ 這支守的是什麼 ═════════════════════════════════════════════════════
|
||
# 三支閘(prod-write/main-and-prod-push/stage-before-prod)都是
|
||
# 「擋下來,但**總管看過就可以放行**」的設計。既有的測試只驗了「擋得住」,
|
||
# **一條都沒有驗過「放得開」**——於是 2026-08-28 才發現:
|
||
# `stat -f %m` 在 GNU coreutils 上是「檔案系統資訊」,它一邊回非零一邊吐一整段文字,
|
||
# 把接在後面的秒數污染掉 ⇒ **在 Linux(=每一個雲端 session)上,那三支閘的戳記
|
||
# 永遠不會被接受** ⇒ 它們在雲端等於純擋,而閘不會告訴你門是壞的。
|
||
#
|
||
# 所以這支的第一優先是「門打得開」,第二優先是「門沒有因此變寬」。
|
||
#
|
||
# 🔴 這支會真的寫 /tmp 的戳記檔(那是閘寫死的路徑,沒有覆寫的開關)。
|
||
# 每一條測完就把它刪掉;跑之前若有真的在等的戳記,會被這支洗掉——
|
||
# 請不要在「已經蓋好戳記正要推東西」的當下跑它。
|
||
set -u
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # hooks/
|
||
REPO="$(cd "$ROOT/.." && pwd)"
|
||
GATEOK="$REPO/scripts/gate-ok"
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"; rm -f /tmp/.prod-write-ok /tmp/.main-push-ok /tmp/.stage-verified /tmp/.solo-ok-TESTSID' EXIT
|
||
export GIT_CONFIG_GLOBAL="$TMP/gitconfig"; : > "$GIT_CONFIG_GLOBAL"
|
||
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
||
|
||
PASS=0; FAIL=0
|
||
ok(){ if [ "$2" = "$3" ]; then printf ' ✅ %s\n' "$1"; PASS=$((PASS+1));
|
||
else printf ' ❌ %s —— 期望 %s,實得 %s\n' "$1" "$3" "$2"; FAIL=$((FAIL+1)); fi; }
|
||
|
||
mkrepo(){ d="$TMP/$1"; git init -q -b main "$d"; ( cd "$d" && echo a>a && git add a && git commit -qm x ) >/dev/null 2>&1; printf '%s\n' "$d"; }
|
||
fire_prod(){ printf '{"tool_name":"Bash","tool_input":{"command":"acr push x.yaml"}}' \
|
||
| bash "$ROOT/prod-write-guard.sh" >/dev/null 2>&1; echo $?; }
|
||
fire_push(){ printf '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' \
|
||
| ( cd "$1" && bash "$ROOT/main-and-prod-push-guard.sh" >/dev/null 2>&1; echo $? ); }
|
||
|
||
rm -f /tmp/.prod-write-ok /tmp/.main-push-ok /tmp/.stage-verified
|
||
|
||
echo "── 最重要:門打得開(Linux 上 stat -f %m 那個 bug 的迴歸)──────"
|
||
ok "① 沒戳記 → prod-write 擋" "$(fire_prod)" 2
|
||
bash "$GATEOK" prod-write >/dev/null
|
||
ok "② gate-ok prod-write 之後 → 放行" "$(fire_prod)" 0
|
||
ok "③ 單次用完即丟:同一枚不能放行第二次" "$(fire_prod)" 2
|
||
|
||
R1=$(mkrepo r1); R2=$(mkrepo r2)
|
||
ok "④ 沒戳記 → 推 main 擋" "$(fire_push "$R1")" 2
|
||
bash "$GATEOK" main-push "$R1" >/dev/null
|
||
ok "⑤ gate-ok main-push <repo> 之後 → 放行" "$(fire_push "$R1")" 0
|
||
|
||
echo "── 門沒有因此變寬 ────────────────────────────────────────────"
|
||
bash "$GATEOK" main-push "$R1" >/dev/null
|
||
ok "⑥ 替 r1 開的門,r2 走不過(綁 repo 沒鬆)" "$(fire_push "$R2")" 2
|
||
rm -f /tmp/.main-push-ok
|
||
# 過期的戳記不算數:把 mtime 調到 16 分鐘前
|
||
bash "$GATEOK" prod-write >/dev/null
|
||
touch -d '16 minutes ago' /tmp/.prod-write-ok 2>/dev/null || touch -A -001600 /tmp/.prod-write-ok 2>/dev/null
|
||
ok "⑦ 16 分鐘前的戳記 → 過期,照樣擋" "$(fire_prod)" 2
|
||
rm -f /tmp/.prod-write-ok
|
||
# 空內容的 main-push 戳記=萬用鑰匙,08-12 那次穿透的形狀,不准復活
|
||
: > /tmp/.main-push-ok
|
||
ok "⑧ 空內容的 main-push 戳記 → 不算數" "$(fire_push "$R1")" 2
|
||
rm -f /tmp/.main-push-ok
|
||
|
||
echo "── gate-ok 自己:不認得的就要拒絕,別蓋一枚打不開的戳記 ──────"
|
||
bash "$GATEOK" nonsense >/dev/null 2>&1; ok "⑨ 不認得的閘名 → 離開碼 2" "$?" 2
|
||
bash "$GATEOK" solo >/dev/null 2>&1; ok "⑩ solo 沒帶 session id → 離開碼 2" "$?" 2
|
||
bash "$GATEOK" main-push "$TMP" >/dev/null 2>&1; ok "⑪ 不是 git repo → 離開碼 2" "$?" 2
|
||
ok "⑫ 而且**沒有**留下一枚註定打不開的空戳記" "$([ -f /tmp/.main-push-ok ] && echo yes || echo no)" no
|
||
bash "$GATEOK" solo TESTSID >/dev/null 2>&1
|
||
ok "⑬ solo <sid> → 蓋出 /tmp/.solo-ok-<sid>" "$([ -f /tmp/.solo-ok-TESTSID ] && echo yes || echo no)" yes
|
||
bash "$GATEOK" stage-verified >/dev/null 2>&1
|
||
ok "⑭ stage-verified → 蓋出 /tmp/.stage-verified" "$([ -f /tmp/.stage-verified ] && echo yes || echo no)" yes
|
||
|
||
echo "── file_mtime 本身:兩個平台的寫法都要答得出純數字 ────────────"
|
||
. "$ROOT/lib/mtime.sh"
|
||
touch "$TMP/probe"
|
||
M=$(file_mtime "$TMP/probe")
|
||
case "$M" in ''|*[!0-9]*) R=bad ;; *) R=ok ;; esac
|
||
ok "⑮ file_mtime 回純數字(舊寫法在 Linux 上會回一整段文字)" "$R" ok
|
||
ok "⑯ 檔案不存在 → 回 0,不是空字串" "$(file_mtime "$TMP/nope")" 0
|
||
|
||
echo
|
||
printf '通過 %s 條,失敗 %s 條\n' "$PASS" "$FAIL"
|
||
[ "$FAIL" = 0 ] || exit 1
|