4322deb23a
總管自己驗這一格量到 exit=0,我量到 exit=1。**兩個都是真的**,
而分歧本身就是最後一塊拼圖:
**GNU 的 `-f` 是 `--file-system`,布林旗標、不接格式字串**
⇒ `%m` 不是格式,它被當成**另一個檔名運算元**
⇒ 離開碼取決於「cwd 裡有沒有一個叫 `%m` 的檔」:
A. 沒有(一般情況) → exit 1 ⇒ `||` 會跑 ⇒ 正確的秒數接在垃圾後面
B. 剛好有 → exit 0 ⇒ `||` 不會跑 ⇒ 整包連一個數字都沒有
兩種情況閘的結果一樣:MT 都不是純數字、戳記都作廢。(實測 GNU coreutils 9.4,兩種都重現過)
🔴 教訓比原本寫的更尖銳:舊寫法的 `||` fallback 救不了,
**不是因為它沒跑,而是因為「跑不跑」根本不由這支腳本決定**——
它由「cwd 裡有沒有某個檔名」決定。
一個行為取決於 cwd 有沒有某個檔的判斷式,不管跑不跑都是壞的。
⇒ 所以修法不能只是「把順序反過來」,**每一步都要驗它是不是純數字**
(lib/mtime.sh 本來就是這樣寫的,現在把理由寫進去了)。
gate-ok 測試補第 ⑰ 條守這一層:cwd 裡有一個叫 `%m` 的檔時,file_mtime 仍要回純數字。
16 → 17 條,TESTING.md 的 A16 一併更新。
89 lines
5.6 KiB
Bash
Executable File
89 lines
5.6 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
|
||
# 🔴 ⑰ 守的是這個 bug 的最後一層:舊寫法的行為**取決於 cwd 裡有沒有一個叫 `%m` 的檔**
|
||
# (GNU 的 -f 是布林旗標,`%m` 被當成另一個檔名運算元)。有 ⇒ exit 0 ⇒ `||` 連跑都不跑。
|
||
# 一個行為取決於 cwd 有沒有某個檔的判斷式,不管跑不跑都是壞的。
|
||
M2=$( cd "$TMP" && touch '%m' && file_mtime "$TMP/probe" )
|
||
case "$M2" in ''|*[!0-9]*) R2=bad ;; *) R2=ok ;; esac
|
||
ok "⑰ cwd 裡有一個叫 %m 的檔 → 仍要回純數字" "$R2" ok
|
||
|
||
echo
|
||
printf '通過 %s 條,失敗 %s 條\n' "$PASS" "$FAIL"
|
||
[ "$FAIL" = 0 ] || exit 1
|