Files
ISEP/hooks/tests/factory-idle-guard.test.sh
T
Leo e3d05df341 再修兩類:主詞是 leo 的下一步、票號放寬把閘變鈍——都是真 transcript 量出來的
交付警察擋回來是對的:前一顆只驗了「我自己造的假 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>
2026-08-23 17:54:17 +08:00

202 lines
8.8 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
# 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 ]