#!/usr/bin/env bash # history-first-guard.sh 的迴歸測試 —— 重點在**逃生門真的走得通**(inkstone/ISEP#122) # # ── 這支在守什麼 ──────────────────────────────────────────────────── # 2026-09-02 實撞:KBDB 兩支工具都回 `Connection closed`,照這支閘印出來的那行 # `touch /tmp/.kbdb-down` # 打完再送同一個編輯,**被一模一樣地擋第二次**——因為 `touch` 造的是空檔, # 而舊版是 `cat` 檔案內容當時戳,空字串被當成 0 ⇒ 「距今 17 億秒」⇒ 永遠不新鮮。 # # 🔴 一道閘印出來的出路走不通,比沒有出路更糟:人照做了、還是被擋, # 下一步就是去繞過這道閘(本 repo 的 ticket-api-bypass-guard.sh 檔頭記過同一課)。 # 所以這支測試的第一等公民是**逃生門那一條**,不是「擋不擋得住」。 # # 🔴 **這支測試自己怎麼被證明有鑑別力**(不要只看它全綠): # 把修好之前那一版拿來跑,第 ④ 條要紅。因為舊版沒有 KBDB_STAMP_DIR, # 直接餵它會去讀這台機器真正的 /tmp 戳記而「剛好過關」,所以要先把路徑隔離: # git show <修好前的 commit>:hooks/history-first-guard.sh > /tmp/old.sh # sed 's#/tmp/\.kbdb-asked#${KBDB_STAMP_DIR:-/tmp}/.kbdb-asked#; # s#/tmp/\.kbdb-down#${KBDB_STAMP_DIR:-/tmp}/.kbdb-down#' /tmp/old.sh > /tmp/old-iso.sh # bash hooks/tests/history-first-guard.test.sh /tmp/old-iso.sh # 2026-09-02 實跑:舊版 9/10(④ 紅),修好後 10/10。 # ⚠️ 第 ③ 條**兩版都是綠的**——它只驗離開碼 2,而過了第 0 道之後歷史警察照樣會擋。 # 真正分辨得出來的是 ④「擋人的是誰」。這一格是本支最容易寫成假綠的地方。 # # 🔴 全程用 KBDB_STAMP_DIR 指到 TMP,**不碰這台機器真正的 /tmp/.kbdb-* 戳記** # (把別的 session 的戳記清掉=把閘弄成隨機的)。不打網路。 set -u HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/history-first-guard.sh}" TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT export KBDB_STAMP_DIR="$TMP/stamps"; mkdir -p "$KBDB_STAMP_DIR" PASS=0; FAIL=0; N=0 # 造一個「有前科」的 code 檔:放在一個有 git 歷史的假 repo 裡 REPO="$TMP/repo"; mkdir -p "$REPO" ( cd "$REPO" && git init -q && git config user.email t@t && git config user.name t \ && printf 'echo old\n' > victim.sh && git add . && git commit -qm "第一版" ) >/dev/null 2>&1 VICTIM="$REPO/victim.sh" # run <檔案> → 印出離開碼(送一份 Edit 的 payload 進去) run() { printf '{"tool_input":{"file_path":"%s"}}' "$1" \ | CLAUDE_PROJECT_DIR="$REPO" bash "$HOOK" >/dev/null 2>"$TMP/err" printf '%s' "$?" } check() { # check <說明> <實際> <期望> N=$((N+1)) if [ "$2" = "$3" ]; then printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)) else printf ' ❌ %s —— 期望離開碼 %s,實際 %s\n' "$1" "$3" "$2"; sed 's/^/ /' "$TMP/err" | head -6; FAIL=$((FAIL+1)); fi } clean_stamps() { rm -f "$KBDB_STAMP_DIR/.kbdb-asked" "$KBDB_STAMP_DIR/.kbdb-down"; rm -f /tmp/.history-guard-*; } echo "── 第 0 道:問過 KBDB 了沒 ──────────────────────────────────" clean_stamps check "① 沒有任何戳記 → 擋" "$(run "$VICTIM")" 2 grep -q "KBDB" "$TMP/err" && printf ' ✅ ② 擋下時訊息講得出是哪一道\n' && PASS=$((PASS+1)) || { printf ' ❌ ② 訊息沒提到 KBDB\n'; FAIL=$((FAIL+1)); }; N=$((N+1)) echo "── 🔴 逃生門:照訊息打那一行,就要真的過得去 ──────────────────" clean_stamps touch "$KBDB_STAMP_DIR/.kbdb-down" # ← 訊息裡原文那一行做的事 check "③ touch 出來的空檔(本票的實撞)→ 要放行過第 0 道" "$(run "$VICTIM")" 2 # ↑ 期望仍是 2:過了第 0 道之後會被「歷史警察」擋(這個檔真的有前科), # 所以要看訊息換人了沒——這是這支測試最容易寫錯的一格。 grep -q "歷史警察" "$TMP/err" && printf ' ✅ ④ 擋人的換成歷史警察 ⇒ 第 0 道確實被逃生門放過了\n' && PASS=$((PASS+1)) || { printf ' ❌ ④ 還是卡在 KBDB 那一道 ⇒ 逃生門是假的\n'; sed 's/^/ /' "$TMP/err" | head -4; FAIL=$((FAIL+1)); }; N=$((N+1)) clean_stamps date +%s > "$KBDB_STAMP_DIR/.kbdb-down" # 舊格式(檔案內容是時戳) run "$VICTIM" >/dev/null grep -q "歷史警察" "$TMP/err" && printf ' ✅ ⑤ 舊格式(內容寫時戳)照樣認得 ⇒ 沒有把既有的弄壞\n' && PASS=$((PASS+1)) || { printf ' ❌ ⑤ 舊格式壞了\n'; FAIL=$((FAIL+1)); }; N=$((N+1)) clean_stamps date +%s > "$KBDB_STAMP_DIR/.kbdb-asked" run "$VICTIM" >/dev/null grep -q "歷史警察" "$TMP/err" && printf ' ✅ ⑥ 真的問過 KBDB(kbdb-asked-stamp.sh 寫的戳記)→ 過第 0 道\n' && PASS=$((PASS+1)) || { printf ' ❌ ⑥ 問過了還被擋\n'; FAIL=$((FAIL+1)); }; N=$((N+1)) echo "── 過期的戳記不算數(逃生門不能變成永久後門)──────────────────" clean_stamps touch -t "$(date -v-2H '+%Y%m%d%H%M' 2>/dev/null || date -d '2 hours ago' '+%Y%m%d%H%M')" "$KBDB_STAMP_DIR/.kbdb-down" run "$VICTIM" >/dev/null grep -q "KBDB" "$TMP/err" && printf ' ✅ ⑦ 兩小時前的空戳記 → 過期,回到擋\n' && PASS=$((PASS+1)) || { printf ' ❌ ⑦ 過期的戳記還在放行 ⇒ 一次 touch 就永久免疫\n'; FAIL=$((FAIL+1)); }; N=$((N+1)) echo "── 不該擋的(誤攔比漏擋嚴重)────────────────────────────────" clean_stamps printf '# doc\n' > "$REPO/readme.md" check "⑧ 文件檔(.md)→ 一路放行,連第 0 道都不該碰" "$(run "$REPO/readme.md")" 0 clean_stamps check "⑨ 不存在的新檔 → 放行" "$(run "$REPO/brand-new.sh")" 0 clean_stamps mkdir -p "$REPO/tests"; printf 'echo t\n' > "$REPO/tests/a.sh" check "⑩ 測試檔 → 放行" "$(run "$REPO/tests/a.sh")" 0 echo printf '通過 %s 條,失敗 %s 條(共 %s 條)\n' "$PASS" "$FAIL" "$N" [ "$FAIL" = 0 ] || exit 1