Files
ISEP/hooks/tests/unpushed-police.test.sh
T
claude-code 352d7a0b81 未推警察改用「遠端有沒有這顆 commit」判斷,不再用「有沒有 upstream」(inkstone/ISEP#90 ①)
雲端 session 開出來的工作分支天生沒有 upstream,內容卻等於遠端 main
(薄殼 HEAD 081c547 = GitHub 遠端 main 081c547,同一顆)
⇒ 每個雲端 session、每次收工都被攔一次,08-27 一個 session 五次全是誤報。

三件:
· 判準改成問遠端(git ls-remote,快取 120 秒)。本機只答得準「有」,
  答「沒有」時才打網路——實測本 session 的 origin/main 落後遠端 4 天。
· 三態:遠端有/遠端沒有/問不到。**問不到一律不報**,不拿離線當罪證。
· 散落分支的基準也一起換成遠端實際那顆——基準過期會把已經在遠端 main 上的
  分支整批報成散落(包含雲端 session 自己那條)。

順手補雲端那格接線:`$CLAUDE_PROJECT_DIR` 在雲端是薄殼根,真身在 `$TOP/InkStoneCo/`。
舊的掃描清單四個路徑一個都不存在 ⇒ 真身有東西沒推,這支閘一輩子不會知道。

測試 hooks/tests/unpushed-police.test.sh:10/10(全離線,本機 bare repo 當遠端)。
拿舊版跑同一份:A 群 5 條全紅、B 群 4 條全綠 ⇒ 測試有鑑別力,不是改到寬鬆。
2026-08-28 00:00:36 +00:00

138 lines
6.2 KiB
Bash
Executable File
Raw 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
# unpushed-police.sh 的迴歸測試(inkstone/ISEP#90
#
# 這支存在的理由是一個**穩定重現**的誤報:雲端 session 開出來的工作分支
# 天生沒有 upstream,內容卻等於遠端 main ⇒ 每個雲端 session、每次收工都被攔一次。
# 2026-08-27 一個 session 攔五次,五次全是誤報。
#
# 兩個方向都要有證據,而且 **A 群(不該報)比 B 群更重要**:
# 永遠在響的警報=訓練人忽略這個警報,下一條真的失蹤的分支會混在雜訊裡。
#
# 🔴 全程離線:用本機 bare repo 當「遠端」,`git ls-remote` 打的是檔案路徑。
# 不碰任何真 repo、不打網路、不寫 /tmp 的正式快取(走 UNPUSHED_CACHE_DIR)。
set -u
HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/unpushed-police.sh}"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' 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; N=0
# mk <名字> → 造一個「遠端 bare + 本機 clone」的場,印出本機 repo 路徑
mk() {
d="$TMP/$1"; mkdir -p "$d"
git init -q --bare "$d/remote.git"
git init -q -b main "$d/work"
( cd "$d/work"
echo a > a.txt; git add a.txt; git commit -qm first
git remote add origin "$d/remote.git"
git push -q origin main
git branch --unset-upstream 2>/dev/null || true ) >/dev/null 2>&1
printf '%s\n' "$d/work"
}
# fire <repo> <期望 exit> <說明> [期望訊息片段]
fire() {
repo="$1"; want="$2"; desc="$3"; want_msg="${4:-}"
N=$((N+1))
out=$(printf '{}' | CLAUDE_PROJECT_DIR="$repo" UNPUSHED_CACHE_DIR="$TMP/cache-$N" \
bash "$HOOK" 2>&1); rc=$?
mkdir -p "$TMP/cache-$N" 2>/dev/null
ok=1
[ "$rc" -eq "$want" ] || ok=0
if [ -n "$want_msg" ] && ! printf '%s' "$out" | grep -qF "$want_msg"; then ok=0; fi
if [ "$ok" = 1 ]; then
printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
else
printf ' ❌ %s —— 期望 exit=%s%s,實得 exit=%s\n' "$desc" "$want" \
"${want_msg:+ 且訊息含「$want_msg}" "$rc"
printf '%s\n' "$out" | sed -n '1,14p' | sed 's/^/ /'
FAIL=$((FAIL+1))
fi
}
# 每個案例先把快取目錄準備好(fire 用的是自己那一格,互不污染)
for i in $(seq 1 40); do mkdir -p "$TMP/cache-$i"; done
echo "── A 群:不該報(誤攔比漏擋更該修)──────────────────────────"
# ① 這就是雲端那個現場:分支沒有 upstream,但它那顆 commit 就是遠端 main 那顆
r=$(mk cloud)
( cd "$r" && git checkout -q -b claude/session-xyz && git branch -D main -q 2>/dev/null
git remote set-head origin -d 2>/dev/null; git update-ref -d refs/remotes/origin/main ) >/dev/null 2>&1
fire "$r" 0 "① 沒 upstream,但遠端就有這顆 commit(雲端每個 session 的現場)"
# ② HEAD 是遠端 main 的**祖先**(遠端已經走在前面)——同樣不是「沒推」
r=$(mk ancestor)
( cd "$r"
echo b > b.txt && git add b.txt && git commit -qm second && git push -q origin main
git checkout -q -b work HEAD~1
git update-ref -d refs/remotes/origin/main ) >/dev/null 2>&1
fire "$r" 0 "② HEAD 是遠端 main 的祖先 → 遠端已經有它"
# ③ 本機的 remote-tracking ref **過期**08-28 實測落後遠端 4 天的那一格)
r=$(mk stale)
( cd "$r"
old=$(git rev-parse HEAD)
echo b > b.txt && git add b.txt && git commit -qm second && git push -q origin main
git update-ref refs/remotes/origin/main "$old" # 本機那份停在舊的
git checkout -q -b claude/session-abc ) >/dev/null 2>&1
fire "$r" 0 "③ 本機 origin/main 過期,但遠端真的有這顆 → 不報"
# ④ 完全問不到遠端(沒有任何 remote)→ **不當成沒推**
r="$TMP/noremote"; git init -q -b main "$r"
( cd "$r" && echo a > a.txt && git add a.txt && git commit -qm only ) >/dev/null 2>&1
fire "$r" 0 "④ 沒有任何 remote=問不到 → 不報(不拿離線當罪證)"
# ⑤ 基準過期時,已經在遠端 main 上的分支不算散落
r=$(mk straystale)
( cd "$r"
old=$(git rev-parse HEAD)
git checkout -q -b feat/done
echo b > b.txt && git add b.txt && git commit -qm done
git push -q origin feat/done:main # 遠端 main 已經含它
git update-ref refs/remotes/origin/main "$old" ) >/dev/null 2>&1
fire "$r" 0 "⑤ 分支已在遠端 main 上,只是本機基準舊 → 不算散落"
# ⑥ stop_hook_active → 一律放行
r=$(mk stopactive)
( cd "$r" && echo x >> a.txt ) >/dev/null 2>&1
N=$((N+1))
out=$(printf '{"stop_hook_active":true}' | CLAUDE_PROJECT_DIR="$r" \
UNPUSHED_CACHE_DIR="$TMP/cache-$N" bash "$HOOK" 2>&1); rc=$?
if [ "$rc" -eq 0 ]; then printf ' ✅ %s\n' "⑥ stop_hook_activetrue → 放行"; PASS=$((PASS+1))
else printf ' ❌ %sexit=%s\n' "⑥ stop_hook_activetrue → 放行" "$rc"; FAIL=$((FAIL+1)); fi
echo "── B 群:該報(不能為了不吵就變成放行)────────────────────"
# ⑦ 真的有遠端沒有的 commit = 真的沒推
r=$(mk unpushed)
( cd "$r"
git checkout -q -b feat/real
echo b > b.txt && git add b.txt && git commit -qm "沒推的那筆" ) >/dev/null 2>&1
fire "$r" 2 "⑦ 分支有遠端沒有的 commit → 要報" "從未推過"
# ⑧ 有改動沒 commit
r=$(mk dirty)
( cd "$r" && echo more >> a.txt ) >/dev/null 2>&1
fire "$r" 2 "⑧ 工作區有未 commit 的改動 → 要報" "未 commit"
# ⑨ 散落分支:本地分支有遠端 main 沒有的東西
r=$(mk stray)
( cd "$r"
git checkout -q -b feat/lost
echo c > c.txt && git add c.txt && git commit -qm "躺著的那筆"
git checkout -q main ) >/dev/null 2>&1
fire "$r" 2 "⑨ 有分支沒併回、遠端也沒有 → 要報散落" "feat/lost"
# ⑩ 判準沒有滑回「有沒有 upstream」:有 upstream 但真的領先,照樣要報
r=$(mk ahead)
( cd "$r"
git branch --set-upstream-to=origin/main main
echo b > b.txt && git add b.txt && git commit -qm "領先一筆" ) >/dev/null 2>&1
fire "$r" 2 "⑩ 有 upstream 且真的領先 → 照樣要報" "領先"
echo
printf '通過 %s 條,失敗 %s 條(共 %s 條)\n' "$PASS" "$FAIL" "$N"
[ "$FAIL" = 0 ] || exit 1