#!/usr/bin/env bash # milestone-account-guard.sh 的迴歸測試(inkstone/ISEP#85) # # A 群「不該擋」——純讀取/建里程碑/改別的欄位/只是談論它/heredoc 內文/ # 走正門 `milestone-account close`/已經記過帳/解析不出來 # B 群「該擋」 ——真的用 curl/python 把某一個里程碑 PATCH 成 closed,而帳本裡沒有它 # C 群「訊息承諾的出路真的走得通」——記過帳之後立刻不再擋;加了豁免字樣就放行 # # 🔴 全程離線:帳本走 MILESTONE_ACCOUNT_LEDGER 指到 mktemp 的檔,不打 Gitea。 # **誤攔比漏擋更該修**——A 群任何一條紅,就是這支閘在懲罰謹慎。 set -u HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/milestone-account-guard.sh}" TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT LED="$TMP/ledger.jsonl"; : > "$LED" PASS=0; FAIL=0; N=0 fire() { want="$1"; desc="$2"; cmd="$3" N=$((N+1)) payload=$(python3 -c 'import json,sys; print(json.dumps({"tool_input":{"command":sys.argv[1]}}))' "$cmd") out=$(printf '%s' "$payload" | MILESTONE_ACCOUNT_LEDGER="$LED" bash "$HOOK" 2>&1 >/dev/null); rc=$? if [ "$rc" -eq "$want" ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1)) else printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s\n' "$desc" "$want" "$rc" printf '%s\n' "$out" | sed -n '1,6p' | sed 's/^/ /'; FAIL=$((FAIL+1)) fi } CLOSE='curl -s -X PATCH -H "Authorization: token X" -d {"state":"closed"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones/12' echo "── A 群:不該擋(誤攔比漏擋更該修)─────────────────────────" fire 0 "① 空指令" "" fire 0 "② 純 GET 撈里程碑" \ 'curl -s https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones/12' fire 0 "③ 列出所有里程碑" \ 'curl -s "https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones?state=open"' fire 0 "④ 建一個新里程碑(那是 milestone-due-guard 管的,不是這支)" \ 'curl -X POST -d {"title":"x","due_on":"2026-09-05T23:59:59Z"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones' fire 0 "⑤ PATCH 但只是改期限,沒有要關它" \ 'curl -X PATCH -d {"due_on":"2026-09-05T23:59:59Z"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones/12' fire 0 "⑥ 只是談論它(echo 在指令位置,curl 在字串裡)" \ "echo '$CLOSE'" fire 0 "⑦ 讀這支閘自己的原始碼" 'cat hooks/milestone-account-guard.sh' fire 0 "⑧ commit 訊息裡出現「關掉 milestone」" \ 'git commit -m "關掉 milestone 12,state closed"' fire 0 "⑨ 走正門:scripts/milestone-account close" \ 'python3 scripts/milestone-account close inkstone/ISEP#12 --reason idle' fire 0 "⑩ 正門的 --dry-run" \ 'python3 scripts/milestone-account close inkstone/ISEP#12 --reason idle --dry-run' fire 0 "⑪ 把那段指令寫進文件(heredoc 內文是資料不是指令)" \ "$(printf 'cat > docs/x.md <<%s\n%s\nEOD\n' "'EOD'" "$CLOSE")" fire 0 "⑫ 網址沒有里程碑編號(抽不出目標 ⇒ 不擋)" \ 'curl -X PATCH -d {"state":"closed"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones' fire 0 "⑬ 關的是一張票不是里程碑" \ 'curl -X PATCH -d {"state":"closed"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/12' echo "── B 群:該擋 ──────────────────────────────────────────────" fire 2 "⑭ curl PATCH state=closed,帳本裡沒有它" "$CLOSE" fire 2 "⑮ 換成 python urllib(method=\"PATCH\")一樣擋" \ 'python3 -c "import urllib.request,json; urllib.request.urlopen(urllib.request.Request(\"https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones/12\", data=json.dumps({\"state\":\"closed\"}).encode(), method=\"PATCH\"))"' fire 2 "⑯ 前面串一個無害指令也擋(不是靠前綴判斷)" "ls && $CLOSE" fire 2 "⑰ --request PATCH 的長寫法" \ 'curl --request PATCH --data {"state":"closed"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones/12' fire 2 "⑱ 別的 repo 的里程碑一樣管" \ 'curl -X PATCH -d {"state":"closed"} https://git.uncle6.me/api/v1/repos/inkstone/arcrun-rag/milestones/47' echo "── C 群:訊息承諾的出路真的走得通 ──────────────────────────" N=$((N+1)) out=$(python3 -c 'import json,sys; print(json.dumps({"tool_input":{"command":sys.argv[1]}}))' "$CLOSE" \ | MILESTONE_ACCOUNT_LEDGER="$LED" bash "$HOOK" 2>&1 >/dev/null) if printf '%s' "$out" | grep -q "scripts/milestone-account audit inkstone/ISEP#12"; then printf ' ✅ ⑲ 訊息直接給出可以照打的那一行(含正確的 owner/repo#編號)\n'; PASS=$((PASS+1)) else printf ' ❌ ⑲ 訊息沒有點名目標\n'; FAIL=$((FAIL+1)) fi echo '{"ref": "inkstone/ISEP#12", "reason": "idle"}' >> "$LED" fire 0 "⑳ 記過帳之後立刻放行(正門是先記帳再關)" "$CLOSE" fire 2 "㉑ 但別的里程碑還是照擋(不是一記帳就全開)" \ 'curl -X PATCH -d {"state":"closed"} https://git.uncle6.me/api/v1/repos/inkstone/ISEP/milestones/13' fire 0 "㉒ 明講的豁免字樣(留痕)" "$CLOSE # milestone-account-ok" echo echo "通過 $PASS 條,失敗 $FAIL 條(共 $N 條)" [ "$FAIL" -eq 0 ]