#!/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 ]