#!/bin/bash
# 交棒警察的測試——三格(指派/tag/下一步)各自缺漏時要被抓到
# 判準:前景 5 種該報、5 種不該報;背景派工(inkstone/ISEP#153)送出時不報、交件時才報、只報一次;
# 訊息印的出路照著貼真的跑得完。
# 用 fixture 跑,不打網路、不在票池留下任何一張測試票。
cd "$(dirname "$0")/.." || exit 1
ROOT=$(pwd)
H=hooks/baton-handback-guard.sh
PASS=0; FAIL=0
FX=$(mktemp -d); ST=$(mktemp -d); trap 'rm -rf "$FX" "$ST"' EXIT
export CLAUDE_PLUGIN_ROOT="$ROOT" ISEP_BATON_STATE_DIR="$ST"
mkfx(){ # $1=state $2=assignees(csv) $3=labels(csv) $4=有沒有交棒留言(y/n)
python3 - "$FX" "$1" "$2" "$3" "$4" <<'PY'
import json, sys, os
d, state, asg, labs, baton = sys.argv[1:6]
issue = {"state": state,
"assignees": [{"login": a} for a in asg.split(",") if a],
"labels": [{"name": l} for l in labs.split(",") if l]}
comments = [{"body": "一般進度留言"}]
if baton == "y":
comments.append({"body": "🏃 **棒子交回** → `claude-code`\n\n**下一步**:去驗那一格"})
json.dump(issue, open(os.path.join(d, "issue.json"), "w"))
json.dump(comments, open(os.path.join(d, "comments.json"), "w"))
PY
}
judge(){ # $1=want $2=got $3=說明
if [ "$2" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s %s\n' "$1" "$2" "$3"
}
run(){ # $1=want $2=說明 (fixture 已備好)
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\n做點事"}}' \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
judge "$1" "$?" "$2"
}
echo "── 該報:三格有缺 ──"
mkfx open "" "s/doing" n ; run 2 "三格全缺(=08-26 那次掉棒的狀態)"
mkfx open "" "s/review" y ; run 2 "缺指派:撈 assignee 撈不到它"
mkfx open "claude-code" "" y ; run 2 "缺 tag:看不出它卡在哪一段"
mkfx open "claude-code" "s/review" n ; run 2 "缺下一步:收下的人得重讀整串"
mkfx open "claude-code" "s/doing" y ; run 2 "tag 還停在 s/doing,但這條線收工了"
echo "── 不該報 ──"
mkfx open "claude-code" "s/review" y ; run 0 "三格都齊(交回總管等 review)"
mkfx open "Leo" "s/stage,Human" y ; run 0 "三格都齊(交給 leo 驗收)"
mkfx open "claude-code" "s/pending,p/high" y ; run 0 "卡在依賴上,但三格都齊"
mkfx closed "" "" n ; run 0 "票已關=棒子到終點,沒有下一手"
echo "── 派工單裡沒有【工單】那行 ⇒ 不歸這支管 ──"
printf '%s' '{"tool_name":"Agent","tool_input":{"prompt":"隨便做點事"}}' \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
judge 0 "$?" "沒有票號的派工單"
# ═══ inkstone/ISEP#153:背景派工 ═══════════════════════════════════════
post(){ # $1=tool_response JSON → 離開碼
printf '{"hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"prompt":"【工單】inkstone/arcrun-rag#136\\n做點事","run_in_background":true},"tool_response":%s}' "$1" \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
}
echo "── 驗收 1:背景派工送出的那一刻不報(票上三格全缺也一樣)──"
mkfx open "" "s/doing" n
post '{"isAsync":true,"status":"async_launched","agentId":"a93f7dc98c50fc671","description":"x"}'
judge 0 "$?" "launch ack(toolUseResult 真跡的形狀:isAsync/async_launched)"
post '[{"type":"text","text":"Async agent launched successfully. (This tool result is internal metadata)\nagentId: a1"}]'
judge 0 "$?" "launch ack 只剩文字 content 陣列時也認得"
post '{"status":"completed","content":[{"type":"text","text":"工作做完了"}]}'
judge 2 "$?" "驗收 3 對照:前景派工回來(不是 launch ack)照舊要報"
# 假 transcript:派工 tool_use + launch ack +(可選)完工通知
TP="$ST/main.jsonl"
mktx(){ # $1=通知對到哪個 tool-use-id(空=還沒交件) $2=通知 status $3=派工單第一行
python3 - "$TP" "$1" "$2" "$3" <<'PY'
import json, sys
tp, note_id, status, first = sys.argv[1:5]
L = []
def add(o): L.append(json.dumps(o, ensure_ascii=False))
for tid, line in (("toolu_AAA", first), ("toolu_BBB", "【工單】inkstone/ISEP#999")):
add({"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "tool_use", "id": tid, "name": "Agent",
"input": {"prompt": line + "\n做點事", "run_in_background": True}}]}})
add({"type": "user", "message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": tid,
"content": [{"type": "text", "text": "Async agent launched successfully."}]}]},
"toolUseResult": {"isAsync": True, "status": "async_launched"}})
if note_id:
n = ("\na1\n%s\n"
"%s\nAgent finished\n好了\n"
"") % (note_id, status)
# 真跡裡同一則通知出現三次:enqueue/attachment/remove
add({"type": "queue-operation", "operation": "enqueue", "content": n})
add({"type": "attachment", "attachment": {"type": "queued_command", "prompt": n}})
add({"type": "queue-operation", "operation": "remove", "content": n})
open(tp, "w").write("\n".join(L) + "\n")
PY
}
stop(){ # $1=session id → 離開碼;stderr 存到 $ST/err
printf '{"hook_event_name":"Stop","session_id":"%s","transcript_path":"%s","stop_hook_active":false}' "$1" "$TP" \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>"$ST/err"
}
echo "── 驗收 2:背景工人真的交件時才報 ──"
mkfx open "" "s/doing" n
mktx "" "" "【工單】inkstone/arcrun-rag#136"
stop s1; judge 0 "$?" "工人還在跑(沒有完工通知)⇒ 不報"
mktx toolu_AAA completed "【工單】inkstone/arcrun-rag#136"
stop s1; got=$?; judge 2 "$got" "完工通知 completed + 票上三格全缺 ⇒ 報"
if grep -q 'inkstone/arcrun-rag#136' "$ST/err" && ! grep -q 'ISEP#999' "$ST/err"; then got=ok; else got=bad; fi
judge ok "$got" "只點名那一則通知對到的票(tool-use-id 比對;另一條還在跑的 #999 不點)"
cp "$ST/err" "$ST/msg"
stop s1; judge 0 "$?" "同一則通知第二次 Stop ⇒ 不鬼打牆(只點名一次)"
mktx toolu_AAA killed "【工單】inkstone/arcrun-rag#136"
stop s2; judge 0 "$?" "通知不是 completed(killed)⇒ 不當成交件"
mkfx open "claude-code" "s/review" y
mktx toolu_AAA completed "【工單】inkstone/arcrun-rag#136"
stop s3; judge 0 "$?" "交件時三格已經補齊 ⇒ 安靜(出路:補完就不再出現)"
mkfx open "" "s/doing" n
mktx toolu_AAA completed "隨便做點事,沒有工單"
stop s4; judge 0 "$?" "交件的那條派工單沒有【工單】⇒ 不歸這支管"
printf '{"hook_event_name":"Stop","session_id":"s5","transcript_path":"/nonexistent"}' \
| BATON_GUARD_FIXTURE="$FX" bash "$H" >/dev/null 2>&1
judge 0 "$?" "讀不到 transcript ⇒ 放行"
echo "── 出路:訊息印的那塊指令照著貼真的跑得完 ──"
BLK=$(python3 - "$ST/msg" <<'PY'
import sys, re
t = open(sys.argv[1], encoding="utf-8").read()
m = re.search(r'^\s*("[^"\n]*/scripts/ticket" handback .*?--evidence "[^"\n]*")', t, re.S | re.M)
if not m:
raise SystemExit
b = m.group(1)
b = b.replace("<收下的人第一件事要做什麼,一句話>", "去複驗")
b = re.sub(r"]*>", "s/review", b)
b = b.replace("<實測輸出或 PR 連結>", "測試")
print(b)
PY
)
case "$BLK" in /*|\"/*) got=abs ;; *) got="${BLK:0:30}" ;; esac
judge abs "$got" "出路印的是絕對路徑(InkStoneCo/scripts/ticket 是舊複本,沒有 handback)"
OFF="TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN_CLAUDE_CODE=fake ISEP_API_RETRIES=0"
env $OFF bash -c "$BLK" >/dev/null 2>&1
judge 1 "$?" "★ 原樣貼上只填三格 ⇒ 參數全過、走到網路才停(離開碼 1;2=被自己的用法擋下)"
env $OFF bash -c "${BLK/--next/--nope}" >/dev/null 2>&1
judge 2 "$?" "對照組:拿掉 --next ⇒ 確實跑不完(上一條才有鑑別力)"
echo
echo "$PASS/$((PASS+FAIL)) 通過"
[ "$FAIL" = 0 ]