#!/usr/bin/env bash # dispatch-format-guard.sh 的迴歸測試(inkstone/ISEP#30 comment 4322/4325/4327)。 # # **離線、不打網路、不花錢、每次結果一樣**——這支閘是純結構判斷,沒有語意判官, # 所以它不像 ask-user-question-guard 那樣需要一支 live 測試量準度。 # # 三群,兩個方向都要有證據: # A 群「該放行」——合規的派工(只有票號)、不是派工的動作、沒有【工單】(別人的地盤)、 # 豁免戳記;**而且合規時要真的把共通規定注入出去** # B 群「該擋」 ——真跡一份(那 5 次違規裡唯一還拿得回來的), # 加上一條規則一個最小案例 # C 群「訊息本身」——講得出出路、沒有被 shell 展開 # # 用法:hooks/tests/dispatch-format-guard.test.sh [hook 路徑] set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HOOK="${1:-$(cd "$HERE/.." && pwd)/dispatch-format-guard.sh}" FIX="$HERE/fixtures" PASS=0; FAIL=0; N=0 # payload [tool_name] payload() { python3 - "$1" "${2:-Task}" <<'PY' import json, os, sys src, tool = sys.argv[1], sys.argv[2] prompt = open(src, encoding="utf-8").read() if os.path.isfile(src) else src print(json.dumps({"session_id": "S-TEST", "hook_event_name": "PreToolUse", "tool_name": tool, "tool_input": {"prompt": prompt}}, ensure_ascii=False)) PY } # t <期望 exit> <說明> t() { want="$1"; desc="$2"; body="$3" N=$((N+1)) err=$(printf '%s' "$body" | 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' "$err" | sed -n '1,6p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi } clean() { rm -f /tmp/.dispatch-ok-S-TEST 2>/dev/null || true; } echo "── A 群:該放行 ────────────────────────────────────────────────" clean t 0 "① 合規:整份派工單只有一行票號" \ "$(payload '【工單】inkstone/ISEP#30 → comment 4322')" t 0 "② 合規:兩張票就兩行【工單】" \ "$(payload '【工單】inkstone/ISEP#30 【工單】inkstone/InkStoneCo#55')" t 0 "③ 合規:用 #issuecomment- 定址也認得" \ "$(payload '【工單】inkstone/ISEP#30#issuecomment-4327')" # ── inkstone/ISEP#65:CLAUDE.md 規定的合格格式(全形/半形括號包住 → comment M) # 曾經被本閘自己擋下——這裡把它釘成迴歸測試,不再走回頭路。 t 0 "③b 合規:全形括號包住 → comment M(CLAUDE.md 標準寫法,曾被自己的閘擋下)" \ "$(payload '【工單】inkstone/ISEP#65(→ comment 4515)')" t 0 "③c 合規:半形括號變體" \ "$(payload '【工單】inkstone/ISEP#65(→ comment 4515)')" # ── inkstone/ISEP#65 test 4:不該擋的三種雜訊 ──────────────────────── t 0 "③d 不該擋:票號後面只有空行" \ "$(payload '【工單】inkstone/ISEP#65 ')" t 0 "③e 不該擋:票號後面只有一句「謝謝」(零資訊禮貌收尾,白名單豁免)" \ "$(payload '【工單】inkstone/ISEP#65 謝謝')" t 0 "③f 不該擋:禮貌收尾帶標點也認得(「謝謝!」)" \ "$(payload '【工單】inkstone/ISEP#65 謝謝!')" t 2 "③g 白名單不是漏洞:禮貌詞混在真內容裡照樣算數" \ "$(payload '【工單】inkstone/ISEP#65 辛苦了,順便告訴你 main 現在是 8e28041')" t 0 "④ 不是派工的動作(Bash)→ 一律不管" \ '{"tool_name":"Bash","tool_input":{"command":"ls"}}' t 0 "⑤ 整包不是合法 JSON → fail-open" 'this is not json at all' t 0 "⑥ 沒有【工單】→ 閉嘴,那是 no-ticket-no-dispatch 的地盤(不准兩支閘同時開口)" \ "$(payload '去把 arcrun 那個 worker 修一修,順便跑一下測試')" # 注入:合規時要真的把共通規定送出去(leo 的驗收條件之一—— # 「收工方**沒讀派工單**也知道要貼回原票」) N=$((N+1)) inj=$(printf '%s' "$(payload '【工單】inkstone/ISEP#30')" | bash "$HOOK" 2>/dev/null) if printf '%s' "$inj" | python3 -c ' import sys, json d = json.load(sys.stdin) c = d["hookSpecificOutput"]["additionalContext"] assert "貼回那張票" in c, "沒有交件方式" assert "【身份】" in c, "沒有身份欄規定" assert "不准 push" in c, "沒有 main 紅線" ' 2>/dev/null; then printf ' ✅ ⑦ 合規的派工會被注入共通規定(交件方式+身份欄+不准 push main)\n'; PASS=$((PASS+1)) else printf ' ❌ ⑦ 共通規定沒有被注入,或缺了其中一項\n' printf '%s\n' "$inj" | sed -n '1,4p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi clean touch /tmp/.dispatch-ok-S-TEST t 0 "⑧ 明示豁免戳記在 → 放行一次" \ "$(payload '【工單】inkstone/ISEP#30 這一次真的有例外')" N=$((N+1)) if [ -f /tmp/.dispatch-ok-S-TEST ]; then printf ' ❌ ⑧b 豁免戳記用完沒被消掉(會變成永久開關)\n'; FAIL=$((FAIL+1)) else printf ' ✅ ⑧b 豁免戳記用完就消失,不是永久開關\n'; PASS=$((PASS+1)) fi clean echo echo "── B 群:該擋 ──────────────────────────────────────────────────" t 2 "⑨ **真跡**:產生 ISEP#30 這條線的那一次派工(散文開場+指令圍欄+三點就地+交件)" \ "$(payload "$FIX/real-violation-isep30.txt")" t 2 "⑩ 只多一行散文——沒有「一句而已」這種豁免" \ "$(payload '【工單】inkstone/ISEP#30 記得先讀 CLAUDE.md')" t 2 "⑪ 帶【交件】——那是「每次都一樣」,該進共通規定" \ "$(payload '【工單】inkstone/ISEP#30 【交件】貼回 inkstone/ISEP#30,回報分支名')" t 2 "⑫ 帶【就地】——那是「這次才知道」,該寫進票" \ "$(payload '【工單】inkstone/ISEP#30 【就地】main 現在是 8e7e265')" t 2 "⑬ 帶【人格】——已收回的欄位" \ "$(payload '【工單】inkstone/ISEP#30 【人格】ISEP plugin 維護者')" t 2 "⑭ 票號形狀不對(裸號,跨 repo 會撞號)" \ "$(payload '【工單】#30')" t 2 "⑮ 內容躲在圍欄裡也算數(圍欄只讓裡面的【】不被當欄位,不讓內容變成不存在)" \ "$(payload '【工單】inkstone/ISEP#30 ``` 【就地】main 現在是 8e7e265 ```')" # ── inkstone/ISEP#65 test 5:拿「寫完規則之後總管又犯的 8 次」當回歸樣本 ── # 內容是從各自票的真實內文摘錄(hooks/tests/fixtures/README.md 記著來歷), # 不是想像出來的例子。8 種形狀都要被擋,證明這不是只補了一個特例。 t 2 "⑮b 回歸:ISEP#60 那次的內容(自造第四套機制的診斷)" \ "$(payload "$FIX/real-content-isep60.txt")" t 2 "⑮c 回歸:ISEP#61 那次的內容(push-guard 戳記機制失敗史)" \ "$(payload "$FIX/real-content-isep61.txt")" t 2 "⑮d 回歸:ISEP#64 那次的內容(PR 分診現況數字)" \ "$(payload "$FIX/real-content-isep64.txt")" t 2 "⑮e 回歸:Arcrun#142 那次的內容(庫索引從沒被產生過)" \ "$(payload "$FIX/real-content-arcrun142.txt")" t 2 "⑮f 回歸:Arcrun#144 那次的內容(樹狀圖驗收記錄)" \ "$(payload "$FIX/real-content-arcrun144.txt")" t 2 "⑮g 回歸:Arcrun#165 那次的內容(MCP 徹查現場)" \ "$(payload "$FIX/real-content-arcrun165.txt")" t 2 "⑮h 回歸:arcrun-rag#104 那次的內容(收檔實測數字)" \ "$(payload "$FIX/real-content-arcrunrag104.txt")" t 2 "⑮i 回歸:InkStoneCo#102 那次的內容(milestone 打假背景)" \ "$(payload "$FIX/real-content-inkstoneco102.txt")" echo echo "── C 群:訊息本身 ──────────────────────────────────────────────" clean msg=$(printf '%s' "$(payload "$FIX/real-violation-isep30.txt")" | bash "$HOOK" 2>&1 >/dev/null) N=$((N+1)) if printf '%s' "$msg" | grep -q '這句話換一張票還成立嗎' \ && printf '%s' "$msg" | grep -q '共通規定' \ && printf '%s' "$msg" | grep -q '寫進那張票'; then printf ' ✅ ⑯ 訊息講得出判準與兩條出路(共通規定/寫進票)\n'; PASS=$((PASS+1)) else printf ' ❌ ⑯ 訊息缺了判準或其中一條出路\n' printf '%s\n' "$msg" | sed -n '1,10p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi # 迴歸:訊息裡的反引號一度在別支閘被 shell 當成命令替換執行掉 # (2026-08-26 ask-user-question-guard:閘照擋,但**它教人怎麼解的那兩行變成空白**) N=$((N+1)) if printf '%s' "$msg" | grep -q 'scripts/ticket say' \ && printf '%s' "$msg" | grep -q 'touch /tmp/.dispatch-ok-S-TEST' \ && ! printf '%s' "$msg" | grep -qi 'is a directory\|command not found'; then printf ' ✅ ⑰ 訊息原文照印:反引號沒被當命令執行,出路那兩行沒有變空白\n'; PASS=$((PASS+1)) else printf ' ❌ ⑰ 訊息被 shell 展開了(指令消失,或冒出 shell 錯誤)\n' printf '%s\n' "$msg" | sed -n '1,20p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi # 訊息要點名是哪一格違規(可迭代的最小單位——leo 4325:「照這種散文寫法根本無法迭代」) N=$((N+1)) if printf '%s' "$msg" | grep -q '派工單不只有票號'; then printf ' ✅ ⑱ 訊息點得出違反的是哪一條規則,不是一句籠統的「格式不對」\n'; PASS=$((PASS+1)) else printf ' ❌ ⑱ 訊息沒有點名違反哪一條\n'; FAIL=$((FAIL+1)) fi clean echo echo "══ $PASS/$N 通過${FAIL:+,$FAIL 個失敗} ══" [ "$FAIL" -eq 0 ]