#!/usr/bin/env bash # diagnosis-evidence-guard.sh + investigate-first-stamp.sh 的迴歸測試(inkstone/ISEP#87) # # 票上的四條驗收條件,一條一組: # 1 開一張還沒有人查過的票,在裡面寫「根因是 X」 → 要被擋一次 # 2 派人查過、結論貼回票上了,再寫診斷 → 放行 # 3 擋的訊息要講得出「你還沒派人查這件事」,不是「格式不對」 # 4 只是轉述別人查到的東西(有出處) → **不能被誤擋** # # 🔴 全程離線:不打 Gitea、不開任何測試票、不碰 /tmp 的正式戳記 # (戳記走 `ISEP_STAMP_DIR`)。每次結果一樣——純結構判斷,沒有語意判官。 # # 🔴 這支閘**沒有任何關鍵字比對**(票上明令:「根因」「因為」「應該是」不准當判準)。 # 所以下面「該擋」的案例裡有沒有出現那些詞完全不影響結果—— # ⑬ 就是拿一段**一個那種詞都沒有**的內容,證明判準不是措辭。 # # 用法:hooks/tests/diagnosis-evidence-guard.test.sh [hook 路徑] set -u unset CLAUDE_CODE_CHILD_SESSION HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$HERE/../.." && pwd)" HOOK="${1:-$ROOT/hooks/diagnosis-evidence-guard.sh}" STAMP="$ROOT/hooks/investigate-first-stamp.sh" TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT export ISEP_STAMP_DIR="$TMP" PASS=0; FAIL=0; N=0 bash_payload() { python3 - "$1" "$2" <<'PY' import json, sys print(json.dumps({"session_id": sys.argv[1], "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": sys.argv[2]}}, ensure_ascii=False)) PY } # t <期望 exit> <說明> <指令> [必須出現的字串] t() { want="$1"; desc="$2"; sid="$3"; cmd="$4"; must="${5:-}" N=$((N+1)) err=$(bash_payload "$sid" "$cmd" | bash "$HOOK" 2>&1 >/dev/null); rc=$? ok=1 [ "$rc" -eq "$want" ] || ok=0 if [ -n "$must" ] && ! printf '%s' "$err" | grep -qF "$must"; then ok=0; fi if [ "$ok" -eq 1 ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1)) else printf ' ❌ %s —— 期望 exit=%s%s,實得 exit=%s\n' "$desc" "$want" \ "${must:+ 且訊息含「$must」}" "$rc" printf '%s\n' "$err" | sed -n '1,8p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi } # 派一次工去查某張票(=讓戳記出現) dispatch() { python3 - "$1" "$2" <<'PY' | bash "$STAMP" import json, sys print(json.dumps({"session_id": sys.argv[1], "tool_name": "Task", "tool_input": {"prompt": "【工單】%s" % sys.argv[2]}}, ensure_ascii=False)) PY } # ── 測資:三份內文 ──────────────────────────────────────────────── # ① 沒查過的診斷(0 個出處) cat > "$TMP/diagnosis.md" <<'EOF' 【身份】總管/inkstone/ISEP/- 根因是 KV 額度爆掉,所以執行紀錄寫不進去;我判斷應該改用另一顆資料庫來存, 這樣就不會再撞到額度上限了。這件事拖了很久一直沒解,先照這個方向做下去。 EOF # ② 同一段話,但帶著出處(轉述別人查到的) cat > "$TMP/relayed.md" <<'EOF' 【身份】總管/inkstone/ISEP/- scout 查回來的:寫入那段在 hooks/lib/evidence.py:42,額度是在那裡爆的, 出處 inkstone/Arcrun#142 → comment 4327。我只是把它的結論搬過來,沒有自己加判斷。 EOF # ③ 一段**一個「根因/因為/應該是」都沒有**的長內容,同樣沒有出處 cat > "$TMP/no-keyword.md" <<'EOF' 【身份】總管/inkstone/ISEP/- 那個匯入流程跑不動是設定檔的問題,換掉那個欄位就會通了,我看過一次有印象。 先照這個方向改,改完應該就不會再出現同樣的狀況了,不用另外再查。 EOF # ④ 短回覆 SHORT="收到,我來看" echo "── 驗收 1+3:沒人查過、也沒出處 ⇒ 擋,且說得出「還沒派人查」──" t 2 "① 貼一段沒人查過的診斷到票上 ⇒ 擋" \ S-A "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" t 2 "② 訊息講得出「這個 session 沒有派人去查這張票」(不是籠統的「格式不對」)" \ S-B "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" \ '沒有派人去查這張票' t 2 "③ 訊息要點名是哪一張票(人才知道該派誰去查什麼)" \ S-C "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" 'inkstone/ISEP#87' t 2 "④ 訊息要給兩條出路:先派人查/把出處帶上" \ S-D "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" '先派人去查' t 2 "⑤ 開新票也一樣(新票天生沒人查過,`ticket new`)" \ S-E "python3 scripts/ticket new ISEP -F $TMP/diagnosis.md --title 身為X我要Y我才Z" t 2 "⑥ 側門:直接打 Gitea API POST 留言 ⇒ 同樣擋(認動作的方式不准漏一條路)" \ S-F "curl -X POST -H 'Authorization: token T' -d '{\"body\":\"根因是 KV 額度爆掉,所以執行紀錄寫不進去;我判斷應該改用另一顆資料庫來存,這樣就不會再撞到額度上限了,這件事拖了很久一直沒解,先照這個方向做下去不要再查了\"}' https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/87/comments" t 2 "⑦ 側門:隱式 POST(requests.post)也要認得" \ S-G "python3 -c \"requests.post(url + '/repos/inkstone/ISEP/issues/87/comments', json={'body': '根因是 KV 額度爆掉,所以執行紀錄寫不進去;我判斷應該改用另一顆資料庫來存,這樣就不會再撞到額度上限了,這件事拖了很久一直沒解,先照這個方向做下去不要再查了'})\"" echo "── 驗收 2:派人查過 ⇒ 放行 ─────────────────────────────────" dispatch S-H inkstone/ISEP#87 t 0 "⑧ 同一 session 派過人去查 #87 ⇒ 同一段診斷放行" \ S-H "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" dispatch S-I inkstone/ISEP#86 t 2 "⑨ 派的是**別張票**(#86)⇒ #87 仍然沒人查過 ⇒ 照擋(戳記按票分,不是按 session)" \ S-I "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" echo "── 驗收 4:轉述有出處 ⇒ 不能被誤擋 ─────────────────────────" t 0 "⑩ 帶檔案:行號+票號+comment 號的轉述 ⇒ 放行" \ S-J "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/relayed.md" for pair in "網址:https://git.uncle6.me/inkstone/ISEP/pulls/97" \ "commit:8e28041" \ "檔案路徑:hooks/lib/dispatch_parse.py"; do kind="${pair%%:*}"; token="${pair#*:}" printf '【身份】總管/inkstone/ISEP/-\n那段行為我核對過了,寫在 %s,跟票上寫的不一樣,所以下一步要照那邊的實況走,先不要動別的地方。\n' \ "$token" > "$TMP/one-source.md" t 0 "⑪ 出處只有一個「$kind」也算數 ⇒ 放行" \ "S-K-$kind" "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/one-source.md" done printf '【身份】總管/inkstone/ISEP/-\n實測輸出如下,這是它自己跑出來的,不是我推的:\n```\n$ bash hooks/tests/x.sh\n通過 20 條,失敗 0 條\n```\n' > "$TMP/fenced.md" t 0 "⑫ 貼實測輸出(圍欄)也算出處 ⇒ 放行" \ S-L "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/fenced.md" echo "── 判準不是措辭(本閘沒有關鍵字比對)────────────────────────" t 2 "⑬ 一個「根因/因為/應該是」都沒有的內容,照樣擋(判準是結構不是詞)" \ S-M "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/no-keyword.md" echo "── 不該擋(誤攔比漏擋嚴重)──────────────────────────────────" t 0 "⑭ 一句短回覆(「$SHORT」)⇒ 不可能是診斷,放行" \ S-N "python3 scripts/ticket say inkstone/ISEP#87 --body '$SHORT'" t 0 "⑮ GET 撈票(唯讀)⇒ 放行" \ S-O "curl -s https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/87" t 0 "⑯ ticket where(搜尋)⇒ 不是寫票,放行" \ S-P "python3 scripts/ticket where 診斷 調查 順序" t 0 "⑰ ticket close(關票)⇒ 不是寫內文,放行" \ S-Q "python3 scripts/ticket close inkstone/ISEP#87 --deliverable https://git.uncle6.me/x" t 0 "⑱ 一般指令(ls)⇒ 一律不管" S-R "ls -la" t 0 "⑲ 逃生口 evidence-ok 在指令裡 ⇒ 放行(留痕)" \ S-S "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md # evidence-ok" echo "── 只擋一次,不鬼打牆 ──────────────────────────────────────" t 2 "⑳ 第一次 ⇒ 擋" S-T "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" t 0 "㉑ 同一 session 同一張票第二次 ⇒ 放行(閘是絆一下,不是把人關起來)" \ S-T "python3 scripts/ticket say inkstone/ISEP#87 -F $TMP/diagnosis.md" t 2 "㉒ 同一 session **換一張票** ⇒ 仍然擋(不是一次就永久解鎖)" \ S-T "python3 scripts/ticket say inkstone/Arcrun#142 -F $TMP/diagnosis.md" echo "── 內部壞掉不准把人鎖死 ────────────────────────────────────" t 0 "㉓ 整包不是合法 JSON ⇒ fail-open" S-U '' N=$((N+1)) err=$(printf 'not json at all' | bash "$HOOK" 2>&1 >/dev/null); rc=$? if [ "$rc" -eq 0 ]; then printf ' ✅ ㉔ payload 不是 JSON ⇒ fail-open\n'; PASS=$((PASS+1)) else printf ' ❌ ㉔ payload 壞掉時擋住了(exit=%s)\n' "$rc"; FAIL=$((FAIL+1)); fi echo printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N" [ "$FAIL" -eq 0 ] || exit 1