Files
ISEP/hooks/tests/factory-idle-guard.test.sh
T
Leo ebe71e3283 fix: 三支閘印出來的 ticket 一律絕對路徑,不叫到 InkStoneCo 舊複本(inkstone/ISEP#113)
dispatch-format-guard.sh 的出路 `scripts/ticket say`、factory-idle-guard.sh 主閘與
副閘的 `scripts/ticket where` 原本印相對路徑。相對路徑是對貼上去那個人的 cwd 解析的,
而收工方 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是 2026-08-27 之前的舊複本
(grep -c subtask → 0)⇒ 照著貼會叫到舊的、什麼都不會發生(同 ISEP#112 的病)。

兩支都改成從 ${CLAUDE_PLUGIN_ROOT} 解出絕對路徑再印(沿用 baton-handback-guard 的做法;
baton 本輪未動,早已是絕對路徑)。dispatch 用既有的佔位符替換機制加 __TICKET__;
factory 是 POSIX sh、unquoted heredoc,直接展開 $TICKET,並用 baton 的引號慣例包住路徑。

測試各加一條在驗這件事:
- dispatch-format-guard.test.sh ⑰ 改成驗絕對路徑+確認沒有相對的 scripts/ticket(52/52)
- factory-idle-guard.test.sh 加主閘訊息實跑 + 掃全支源碼兩條(33→35)
- baton 既有 24/24 不動

版本待總管定(plugin.json 未動)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGxitYq49FzYC7EFkbhzF5
2026-09-18 17:45:46 +00:00

231 lines
10 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 "── 出路裡的 ticket 是絕對路徑,不是相對的 scripts/ticketinkstone/ISEP#113)─"
# 收工方的 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是 2026-08-27 之前的舊複本
# `grep -c subtask` → 0)⇒ 相對路徑貼過去會叫到它、什麼都不會發生(同 comment-carries ⑮、
# baton-handback)。這兩條分別驗「主閘印出來的那塊」與「整支閘的訊息裡再也沒有相對路徑」。
N=$((N+1))
mk "$TMP/tr-path.jsonl" 'T:我看完了。下一步我去把那支閘的誤攔補上。'
pmsg=$(printf '{"transcript_path":"%s"}' "$TMP/tr-path.jsonl" \
| CLAUDE_PROJECT_DIR="$TMP" sh "$HOOK" 2>&1)
if printf '%s' "$pmsg" | grep -qE '/scripts/ticket" where' \
&& ! printf '%s' "$pmsg" | grep -qE '`scripts/ticket'; then
printf ' ✅ 主閘出路的 ticket 印絕對路徑,沒有相對的 scripts/ticket\n'; PASS=$((PASS+1))
else
printf ' ❌ 主閘出路又印回相對的 scripts/ticket ⇒ 在 InkStoneCo cwd 貼上去會叫到舊複本\n'
printf '%s\n' "$pmsg" | grep -n 'ticket' | sed 's/^/ /'
FAIL=$((FAIL+1))
fi
# 副閘(全廠停工那塊)在離線測試裡拿不到 gitea token、走不到,所以直接掃這支閘的原始碼:
# 印出來的每一處 ticket 指令都不准是相對路徑(backtick 直接接 scripts/ticket)。
N=$((N+1))
if grep -n '`scripts/ticket' "$HOOK" >/dev/null 2>&1; then
printf ' ❌ 這支閘的訊息裡還有相對路徑的 scripts/ticket(副閘那塊漏改了)\n'
grep -n '`scripts/ticket' "$HOOK" | sed 's/^/ /'
FAIL=$((FAIL+1))
else
printf ' ✅ 整支閘的訊息裡沒有任何相對路徑的 scripts/ticket(主閘+副閘都改了)\n'; PASS=$((PASS+1))
fi
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 ]