e3d05df341
交付警察擋回來是對的:前一顆只驗了「我自己造的假 transcript」。
改用本機一條 2068 行的真 session(26 個真實回合終止點)重驗,當場多找到兩個問題:
① 主詞是「你」的下一步,被當成我的宣告。
舊閘在 26 個真實回合裡擋了 2 次,兩次咬的都是我在交代 leo 該做什麼:
「下一步還是那一個動作:你把 feat/... 併進 main」
「## 你下一步(兩招,先便宜的)」
⇒ 這是全新的第四類誤攔,我原本一向都沒列到。
修法是主詞檢查(誰要動手),只掛在「下一步」這條 alternative 上;
用 finditer 逐個檢查前 8 字有沒有第二人稱,蓋得到「你的下一步」這種
單字 lookbehind 蓋不到的變體。「你點頭我就做」主詞本來就是我,不受影響。
② 票號從「宣告句附近」放寬成「整段」,把閘變鈍了。
真數據:26 個真實回合有 20 個是靠「文中某處剛好有票號」放行的——
而報告幾乎一定會提到票號 ⇒ 這道閘在實務上等於不會響。
當初放寬是因為票號常寫在行內 code 裡,剝掉就找不到。
⇒ 改成**等長**替換(蓋成同樣長度的哨兵而非刪除),位移就能對回原文,
locality 與「行內 code 裡的票號也算數」兩件同時成立。
收緊後的判定分佈:21 no-declaration / 3 dispatched / 2 ticket-referenced
(原本是 20 ticket-referenced / 3 no-declaration / 3 dispatched)。
真 transcript 實測:舊閘擋 2 次(兩次都是誤攔)→ 新閘擋 0 次。
測試 33 向(+6):舊版 21/33 → 新版 33/33。
版號 0.3.7 → 0.3.8。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
202 lines
8.8 KiB
Bash
Executable File
202 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# factory-idle-guard.sh 的迴歸測試(inkstone/ISEP#30)。
|
||
#
|
||
# 來由:2026-08-23 雲端驗收連續三次被這道閘誤攔,三次都不是宣告意圖——
|
||
# ① 「那句是等你,不是我的下一步」 ← 在否認自己有下一步
|
||
# ② 「這不是『下一步我要做 X』」 ← 引用閘自己的訊息來說明沒在做
|
||
# ③ 貼出閘自己的正則原始碼來舉報這個 bug ← code block 裡的引用也被當成宣告
|
||
# 而閘的訊息教人走「選項③:改寫那句話,說明它在等什麼」,程式碼裡卻沒有那條分支。
|
||
#
|
||
# 本檔的兩個方向都要有證據(誤攔與漏攔都要測):
|
||
# A 群「該放行」——上面三種誤攔案例 + 既有放行路徑不准變鬆的回歸
|
||
# B 群「該擋」 ——真的宣告了未來動作、且這回合沒按下任何 trigger
|
||
# C 群「訊息承諾的出路真的走得通」——訊息裡列的每一條,各一個實例
|
||
#
|
||
# 用法:hooks/tests/factory-idle-guard.test.sh [hooks/factory-idle-guard.sh 的路徑]
|
||
# 🔴 全程在一個乾淨的 TMP 底下造假 transcript,跑完自己清;不碰任何真 repo、不打網路。
|
||
# (CLAUDE_PROJECT_DIR 指向沒有 gitea remote 的 TMP ⇒ 副閘拿不到 token,直接 exit 0,
|
||
# 所以本檔測的一律是主閘。)
|
||
|
||
set -u
|
||
HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/factory-idle-guard.sh}"
|
||
TMP=$(mktemp -d)
|
||
trap 'rm -rf "$TMP"' EXIT
|
||
|
||
PASS=0; FAIL=0
|
||
N=0
|
||
|
||
# mk <transcript 路徑> <block spec>...
|
||
# block spec:`T:<文字>` = assistant 的文字段;`U:<工具名>` = assistant 的 tool_use
|
||
mk() {
|
||
python3 - "$@" <<'PY'
|
||
import json, sys
|
||
path, specs = sys.argv[1], sys.argv[2:]
|
||
rows = [{"type": "user", "message": {"content": [{"type": "text", "text": "【工單】去辦"}]}}]
|
||
blocks = []
|
||
for s in specs:
|
||
k, v = s.split(":", 1)
|
||
blocks.append({"type": "text", "text": v} if k == "T"
|
||
else {"type": "tool_use", "name": v, "id": "t1", "input": {}})
|
||
rows.append({"type": "assistant", "message": {"content": blocks}})
|
||
with open(path, "w") as f:
|
||
for r in rows:
|
||
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
||
PY
|
||
}
|
||
|
||
# t <期望 exit> <說明> <block spec>...
|
||
t() {
|
||
want="$1"; desc="$2"; shift 2
|
||
N=$((N+1))
|
||
tr="$TMP/tr-$N.jsonl"
|
||
mk "$tr" "$@"
|
||
out=$(printf '{"transcript_path":"%s"}' "$tr" \
|
||
| CLAUDE_PROJECT_DIR="$TMP" sh "$HOOK" 2>&1); 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
|
||
}
|
||
|
||
echo "── A 群:該放行(2026-08-23 雲端三次誤攔的原形狀)─────────────────"
|
||
|
||
t 0 "① 否認自己有下一步(『那句是等你,不是我的下一步』)" \
|
||
'T:我看完了。那句是等你,不是我的下一步。'
|
||
|
||
t 0 "① 長版:連同被否認的原句一起引用" \
|
||
'T:你貼的那行「回覆後我就繼續」是等你,不是我的下一步。'
|
||
|
||
t 0 "② 引用閘自己的訊息來說明沒在做" \
|
||
'T:這不是『下一步我要做 X』,是在說明那道閘認錯了對象。'
|
||
|
||
t 0 "③ code block 裡貼閘自己的正則原始碼(舉報 bug)" \
|
||
'T:誤攔的真兇是這段:
|
||
```
|
||
r"(下一步(我|就是|是)?[::]?\s*(?!不是宣告)|接下來我|我(現在|接著|等下|等一下)(就)?(去|來|做|派|審|跑)"
|
||
```
|
||
它連裸的「下一步」三個字都吃。'
|
||
|
||
t 0 "③ 行內 code 貼同一段正則" \
|
||
'T:問題出在 `接下來我` 這個 alternative 沒有要求後面接動詞。'
|
||
|
||
t 0 "引用 leo 的話(blockquote)含宣告字樣" \
|
||
'T:> 下一步我去把那支閘修好,你不要插手。
|
||
|
||
以上是他八月十六日的原話,我引來說明這道閘的規格。'
|
||
|
||
t 0 "User Story 票名(身為…我要…我才…)不是宣告" \
|
||
'T:我把票名寫成:身為總管,我要把散落的分支收掉整理好,我才不會每天重數一次。'
|
||
|
||
# ↓ 這兩向不是想出來的,是拿本機一條 2068 行的真 transcript 跑出來的:
|
||
# 舊閘在 26 個真實回合終止點上擋了 2 次,兩次咬的都是「我在交代 leo 該做什麼」。
|
||
t 0 "④ 主詞是 leo 的下一步(『你下一步去把 X 併進 main』)" \
|
||
'T:## 你下一步(兩招,先便宜的)
|
||
|
||
你下一步去把 feat/isep-dynamic-install 併進 main,然後開一個全新雲端 session。'
|
||
|
||
t 0 "④ 同上,交代對方要跑的動作" \
|
||
'T:前三題不適用。下一步還是那一個動作:你把那條分支併進 main,再跑一次看信標印什麼。'
|
||
|
||
t 0 "④ 『你的下一步』——主詞在對方身上(單字 lookbehind 蓋不到的變體)" \
|
||
'T:我把兩條路都寫清楚了。你的下一步去把那條分支併進 main,再開一個新 session 看信標。'
|
||
|
||
t 2 "④ 的反面:主詞換回我自己 ⇒ 照樣擋" \
|
||
'T:我把兩條路都寫清楚了。我的下一步去把那條分支併掉。'
|
||
|
||
echo
|
||
echo "── A 群:既有放行路徑,不准變鬆(迴歸)─────────────────────────"
|
||
|
||
t 0 "純對話回合,沒有任何宣告" \
|
||
'T:這道閘的問題在於它把引用當成宣告,量錯了對象。'
|
||
|
||
t 0 "宣告了,但這個回合真的呼叫了 Agent(=按下 trigger)" \
|
||
'T:下一步我去把 sdd-guard 的誤攔補上。' 'U:Agent'
|
||
|
||
t 0 "宣告了,但這個回合真的呼叫了 Task" \
|
||
'T:接下來我會去補測試。' 'U:Task'
|
||
|
||
t 0 "宣告句帶票號指涉(已經在別人手上)" \
|
||
'T:下一步我去把那支閘補好——已經派給 inkstone/ISEP#30,證據在 comment 3970。'
|
||
|
||
t 0 "宣告之後真的動手了(文字後面還有工具呼叫)" \
|
||
'T:先讀原始碼,接下來我會去補測試。' 'U:Read'
|
||
|
||
echo
|
||
echo "── B 群:該擋(真的宣告了未來動作,且這回合沒按下任何 trigger)────"
|
||
|
||
t 2 "『下一步我去把 X 補上』,無派工無票號" \
|
||
'T:我看完了。下一步我去把那支閘的誤攔補上。'
|
||
|
||
t 2 "『接下來我會去補測試』" \
|
||
'T:目前狀況清楚了。接下來我會去補測試。'
|
||
|
||
t 2 "把請示寫成條件句(2026-08-17 leo 實撞那族)" \
|
||
'T:回「規劃」我就派人盤這份計畫。'
|
||
|
||
t 2 "『我等你確認』——空手停在等人身上" \
|
||
'T:我等你確認。'
|
||
|
||
t 2 "『我現在就去跑一遍』" \
|
||
'T:我現在就去跑一遍那批測試。'
|
||
|
||
t 2 "『確認後我才動』" \
|
||
'T:核准後我才去改那支閘。'
|
||
|
||
t 0 "票號寫在行內 code 裡也算數(等長替換才量得到)" \
|
||
'T:下一步我去把那支閘補好——已派給 `inkstone/ISEP#30`。'
|
||
|
||
t 2 "票號離宣告句太遠(文章別處提到而已)⇒ 不算它在別人手上" \
|
||
'T:早上那批已經在 inkstone/ISEP#30 收掉了。
|
||
|
||
(中略:這裡是一大段跟那張票無關的驗收敘述,用來把距離拉開。'"$(printf 'x%.0s' $(seq 1 320))"')
|
||
|
||
下一步我去把那支閘的誤攔補上。'
|
||
|
||
echo
|
||
echo "── C 群:訊息承諾的每一條出路,都要真的走得通 ─────────────────"
|
||
|
||
t 0 "出路①:這個回合直接呼叫 Agent 派出去" \
|
||
'T:下一步我去把那支閘補好。' 'U:Agent'
|
||
|
||
t 0 "出路②:把票號寫進那句話" \
|
||
'T:下一步我去把那支閘補好(已派給 inkstone/ISEP#30)。'
|
||
|
||
t 0 "出路③:改寫那句話,用 ⏸ 等: 說明它在等什麼" \
|
||
'T:⏸ 等:leo 親手跑 scripts/github-arm.sh 解保險(D20 人閘,不是我能自己按的)。
|
||
|
||
在那之前我等你確認,這件事我不自己動。'
|
||
|
||
t 0 "出路③:宣告 + 標記同時存在也放行(標記說明了它在等什麼)" \
|
||
'T:那批 workflow 要重打 bundle,下一步我去重打。⏸ 等:arcrun-rag 那條 PR 先併,前置沒解除。'
|
||
|
||
t 0 "出路④:這個回合收尾在動作上(最後一個工具呼叫之後沒有文字)" \
|
||
'T:下一步我去把那支閘補好。' 'U:Edit'
|
||
|
||
echo
|
||
echo "── 出路③ 的反面:沒寫標記就照樣擋(不准靠刪掉那句話矇混)───────"
|
||
|
||
t 2 "同一句話拿掉 ⏸ 等: 標記 ⇒ 擋" \
|
||
'T:那批 workflow 要重打 bundle。我等你確認,這件事我不自己動。'
|
||
|
||
echo
|
||
echo "── 壞輸入不准亂擋(fail-open)────────────────────────────────"
|
||
|
||
n_bad=0
|
||
for payload in '{}' '{"transcript_path":"/nonexistent/xx.jsonl"}' 'not-json'; do
|
||
N=$((N+1))
|
||
printf '%s' "$payload" | CLAUDE_PROJECT_DIR="$TMP" sh "$HOOK" >/dev/null 2>&1; rc=$?
|
||
if [ "$rc" -eq 0 ]; then
|
||
printf ' ✅ 壞輸入 %-34s ⇒ exit=0\n' "$payload"; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ 壞輸入 %-34s ⇒ exit=%s(期望 0)\n' "$payload" "$rc"; FAIL=$((FAIL+1))
|
||
fi
|
||
n_bad=$((n_bad+1))
|
||
done
|
||
|
||
echo
|
||
echo "────── 通過 $PASS / 失敗 $FAIL (共 $N 向)"
|
||
[ "$FAIL" -eq 0 ]
|