Files
ISEP/hooks/tests/dispatch-format-guard.test.sh
T
Leo b1830053e5 dispatch-format-guard 修兩處:規則自己的合格範例會被自己擋,禮貌收尾不該算違規
inkstone/ISEP#65:leo 發現「寫完 ISEP#30 那條規則之後,同一天總管又犯了 8 次」,
派來查「現行閘為什麼抓不到」。查證結果分兩層:

━━ 真正在跑的閘其實是 dispatch-format-guard.sh,不是 no-ticket-no-dispatch.sh ━━
no-ticket-no-dispatch.sh 的殼驗證早就是已知行為(只驗有沒有一行【工單】),
但 ISEP#30 已經為此新增了 dispatch-format-guard.sh 做內容判定,測試 19/19 通過。
問題是它有兩個沒被那 19 條測資蓋到的洞:

1. **regex 洞(本體 bug)**:頂層 CLAUDE.md 規定的合格格式帶全形括號——
   「【工單】owner/repo#N(→ comment M)」。_COMMENT_RE 只吃「→ comment M」
   本體,兩側括號沒被算進去,殘留括號讓 _REF_RE 比不過,於是**規則自己定義
   的合格範例會被自己的閘擋下**(半形括號 `(...)` 同樣會中)。用今天派我這張
   票的那份派工單原句實測,改之前 exit=2「票號形狀不對」。
2. **零容忍過頭**:純禮貌收尾(「謝謝」)跟「記得先讀 CLAUDE.md」這種真內容
   一樣被當「派工單不只有票號」擋下。ISEP#65 test 4 明講這四種不該擋
   (只有票號/空行/「謝謝」/括號包住的 comment 格式),優先做成低誤鎖。

判斷:不改 no-ticket-no-dispatch.sh、不另立第三支閘——dispatch-format-guard.sh
已經是「另立一支」的正確位置,這兩個洞在它自己的地盤上補。

修法:
- _COMMENT_RE 兩側括號(全形/半形)都設可選
- 新增 _COURTESY_CLOSERS 白名單(謝謝/多謝/感謝/辛苦了…)+ _is_courtesy_closer(),
  只在圍欄外生效,判準仍是「整行清乾淨標點後完全相等」不是「包含」——
  白名單不是黑名單,猜漏頂多誤鎖一次,不會反過來放走真內容(③g 測資證明)

測試:hooks/tests/dispatch-format-guard.test.sh 19→33 條,全過。新增:
- ③b/③c 全形/半形括號格式(規則自己的例句)
- ③d/③e/③f ISEP#65 test 4 的三種不該擋
- ③g 白名單邊界(禮貌詞混真內容裡照樣算數,防止白名單被誤用成漏洞)
- ⑮b–⑮i:leo 點名的「寫完規則後又犯的 8 次」當回歸樣本,內容是從
  ISEP#60/#61/#64、Arcrun#142/#144/#165、arcrun-rag#104、InkStoneCo#102
  的真實票內文摘錄(見 hooks/tests/fixtures/README.md 記載來歷),
  不是想像出來的例子;8 種形狀全部驗證會被擋
reply-identity.test.sh 11/11 仍全過(共用 dispatch_parse.py 沒有回歸)

升版 0.5.0 → 0.5.1(改完不升版沒人吃得到;check-version-consistency.sh
在本分支照慣例是紅的,tag 於 merge 時打)。

另查:.shell-payload/ 整個被 gitignore(scripts/vendor-to-shell.py 產物),
不是 git 分發的一部分——「這支閘會不會被吃到」取決於消費端有沒有重跑
plugin update/vendor-to-shell.py,不是這個 repo 委交的內容缺漏,故不在
本票改動範圍內,僅記錄供總管排查用。

未動 no-ticket-no-dispatch.sh(判斷見上,職責保持不重疊,兩支閘互斥見
dispatch_parse.py 的 has_ticket_marker 分岔)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:25:17 +08:00

229 lines
9.7 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
# dispatch-format-guard.sh 的迴歸測試(inkstone/ISEP#30 comment 432243254327)。
#
# **離線、不打網路、不花錢、每次結果一樣**——這支閘是純結構判斷,沒有語意判官,
# 所以它不像 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 <prompt 檔或字串> [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> <說明> <payload JSON>
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#65CLAUDE.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 ]