2bbcda2ac3
leo 09-18:「它看到票時要能夠組成 milestone 提出版本,所以不只是看票,還要組票。」 雲端只會撿(ISEP#130),沒有任何機制分診或組里程碑;open 302 張裡 74 張沒 s/*、257 張沒掛里程碑。 - ticket triage:沒 s/* 的票 → s/backlog/s/triage/s/pending+票上一則說明(--why 必填),一個動作; --dup-of:說明+close/duplicate+關+在留下的那張留一則指回來(留下的那張若是 leo 的就不寫) - ticket compose:把已分診的票組成 InkStoneCo 里程碑(開工規約 1–7 條):既有載體+前置相依、 s/backlog → s/todo、名字不准是版本號、期限 1–60 天、不開新票;先全部讀過才寫,理由一次列齊 - 不碰:Human/human/*、指派給別人(含 Leo)——判準是標籤與 assignee 登入名 - pick --pool:還沒到期的 InkStoneCo 里程碑的前置排在散票之前;可做的都抓盡時回待分診的票, 每列帶 next;queue-drain-guard 照 next 印(待分診是 triage,不是 claim) - 全局票池只有一個讀法 pool_issues(),接口留給 ISEP#154(global_index 寫卡驗完後只換這一個函式) - InkStoneCo 掛在 open 里程碑的票=載體(容器):不進票池、不算待分診 假設(寫在這裡等總管 review): - 分診只收「沒有 s/*」的票;s/triage 仍是總管/leo 的判斷,工人只能把拿不準的丟進 s/triage - 載體撈不到時,工人用 ticket where+ticket new 補一張 InkStoneCo User Story 當載體(開工規約第 2 條的「真缺口」) - 期限上限 60 天(9999 與過去日期都擋) 實測:test-ticket-triage 80/80、test-ticket-pick 84/84、queue-drain-guard 28/28; 真 Gitea 代跑一趟:分診 4 張、建 InkStoneCo milestone 68(載體 InkStoneCo#136,前置 ISEP#47、arcrun-rag#199), pick --pool 第一組即為它的前置;一次違規三格的 compose 真的零寫入擋下。 待總管定版。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
6.2 KiB
Bash
Executable File
115 lines
6.2 KiB
Bash
Executable File
#!/bin/bash
|
||
# queue-drain-guard.sh 的迴歸測試(inkstone/ISEP#33)
|
||
#
|
||
# 全程離線:`ticket pick` 換成一支假的(ISEP_DRAIN_PICK_CMD),離開碼與輸出由測試指定。
|
||
# 兩個方向都測:該擋的(在接票、池子有票)與不該擋的(聊天 session、抓盡、鎖死保險)。
|
||
cd "$(dirname "$0")/../.." || exit 1
|
||
H=hooks/queue-drain-guard.sh
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||
PASS=0; FAIL=0
|
||
unset ISEP_DRAIN CLAUDE_PLUGIN_ROOT
|
||
|
||
cat > "$TMP/pick.sh" <<'SH'
|
||
#!/bin/sh
|
||
cat "$STUB_DIR/out" 2>/dev/null
|
||
exit "$(cat "$STUB_DIR/rc")"
|
||
SH
|
||
export STUB_DIR="$TMP" ISEP_DRAIN_PICK_CMD="sh $TMP/pick.sh"
|
||
|
||
stub() { printf '%s' "$1" > "$TMP/rc"; printf '%s' "${2:-}" > "$TMP/out"; }
|
||
TICKET='{"ref":"inkstone/Arcrun#176","title":"查一份筆記關係的人","group":"主線"}'
|
||
|
||
# transcript <檔名> <Bash 指令或空> [純文字]
|
||
transcript() {
|
||
python3 - "$TMP/$1" "$2" "${3:-}" <<'PY'
|
||
import json, sys
|
||
path, cmd, text = sys.argv[1], sys.argv[2], sys.argv[3]
|
||
rows = [{"type": "user", "message": {"content": "開工"}}]
|
||
content = []
|
||
if text: content.append({"type": "text", "text": text})
|
||
if cmd: content.append({"type": "tool_use", "name": "Bash", "input": {"command": cmd}})
|
||
rows.append({"type": "assistant", "message": {"content": content}})
|
||
open(path, "w").write("\n".join(json.dumps(r, ensure_ascii=False) for r in rows))
|
||
PY
|
||
}
|
||
|
||
run() { # run <說明> <want exit> <session> <transcript 檔> [stop_hook_active]
|
||
export ISEP_DRAIN_STATE_DIR="$TMP/state-$3"; mkdir -p "$ISEP_DRAIN_STATE_DIR"
|
||
payload=$(printf '{"session_id":"%s","transcript_path":"%s","stop_hook_active":%s}' "$3" "$TMP/$4" "${5:-false}")
|
||
printf '%s' "$payload" | bash "$H" > "$TMP/stdout" 2> "$TMP/stderr"; got=$?
|
||
if [ "$got" = "$2" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=%s got=%s %s\n' "$2" "$got" "$1"
|
||
}
|
||
check() { # check <說明> <shell 條件>
|
||
if eval "$2"; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf '%s\n' "$1"
|
||
}
|
||
|
||
transcript worker.jsonl 'python3 /x/scripts/ticket claim inkstone/Arcrun#175 --name cloud-worker'
|
||
transcript handback.jsonl 'python3 "$CLAUDE_PLUGIN_ROOT/scripts/ticket" handback inkstone/Arcrun#175 --to claude-code'
|
||
transcript chat.jsonl 'git status'
|
||
transcript textonly.jsonl '' '我等一下會跑 ticket pick 看看'
|
||
|
||
echo "── ① 該擋:在接票、池子還有票 ──"
|
||
stub 0 "$TICKET"
|
||
run "★ 跑過 ticket claim + pick 有票 → 擋" 2 s1 worker.jsonl
|
||
check "訊息點名下一張票號" "grep -q 'inkstone/Arcrun#176' '$TMP/stderr'"
|
||
check "訊息給出認領指令(claim)" "grep -q 'claim inkstone/Arcrun#176' '$TMP/stderr'"
|
||
check "訊息給出「做不了」的出路(寫回票+改欄位),不是叫它停" "grep -q '寫回那張票' '$TMP/stderr'"
|
||
run "引號包住路徑的 handback 也算在接票" 2 s2 handback.jsonl
|
||
run "★ stop_hook_active=true 仍擋(別的閘擋過一次就讓路,正是停工的來源)" 2 s3 worker.jsonl true
|
||
ISEP_DRAIN=1 run "ISEP_DRAIN=1:還沒跑過任何 ticket 指令也擋" 2 s4 chat.jsonl
|
||
check "log 寫在狀態目錄(不靠 CLAUDE_PROJECT_DIR)" "grep -q '擋下' '$TMP/state-s4/queue-drain-guard.log'"
|
||
|
||
stub 0 '{"ref":"inkstone/mira#21","title":"沒人看過的票","group":"待分診","next":"python3 /x/scripts/ticket triage inkstone/mira#21 --to <s/backlog|s/triage|s/pending> --why \"<一句理由>\""}'
|
||
run "★ 可做的都抓盡、還有待分診的票 → 擋(ISEP#159:沒驗傷的票不會自己消失)" 2 s9 worker.jsonl
|
||
check "訊息照 pick 那一列的 next 印 triage,不印 claim(待分診的票認領不了)" \
|
||
"grep -q 'triage inkstone/mira#21' '$TMP/stderr' && ! grep -q 'claim inkstone/mira#21' '$TMP/stderr'"
|
||
|
||
echo
|
||
echo "── ② 不該擋 ──"
|
||
stub 0 "$TICKET"
|
||
run "★ 沒跑過 ticket 指令的 session(跟 leo 聊天)→ 放行" 0 s5 chat.jsonl
|
||
run "文字裡提到 ticket pick、沒真的執行 → 放行(看動作不看措辭)" 0 s6 textonly.jsonl
|
||
stub 1 ""
|
||
run "★ pick 離開碼 1(真的抓盡)→ 放行" 0 s7 worker.jsonl
|
||
printf 'not json' | bash "$H" >/dev/null 2>&1; got=$?
|
||
if [ "$got" = 0 ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=0 got=%s 壞掉的 payload → 放行\n' "$got"
|
||
|
||
echo
|
||
echo "── ③ 鎖死保險 ──"
|
||
stub 0 "$TICKET"
|
||
run "同一張第 1 次 → 擋" 2 s8 worker.jsonl
|
||
run "同一張第 2 次 → 擋" 2 s8 worker.jsonl
|
||
run "同一張第 3 次 → 擋" 2 s8 worker.jsonl
|
||
run "★ 同一張第 4 次仍沒被認領 → 放行(它可能真的做不了)" 0 s8 worker.jsonl
|
||
check "放行理由記進 log(那張要總管看)" "grep -q 'stuck-on:inkstone/Arcrun#176' '$TMP/state-s8/queue-drain-guard.log'"
|
||
stub 0 '{"ref":"inkstone/Arcrun#197","title":"另一張"}'
|
||
run "換了一張 → 計數重來,擋" 2 s8 worker.jsonl
|
||
|
||
stub 2 ""
|
||
run "讀不到佇列第 1 次 → 擋(讀不到 ≠ 沒票)" 2 s9 worker.jsonl
|
||
check "訊息叫它重跑 pick" "grep -q 'pick' '$TMP/stderr' && grep -q '讀不到' '$TMP/stderr'"
|
||
run "讀不到第 2 次 → 擋" 2 s9 worker.jsonl
|
||
run "★ 讀不到第 3 次 → 放行並留痕" 0 s9 worker.jsonl
|
||
check "log 有 unreachable" "grep -q 'unreachable' '$TMP/state-s9/queue-drain-guard.log'"
|
||
|
||
printf '0' > "$TMP/rc"; printf '%s' "$TICKET" > "$TMP/out"
|
||
ISEP_DRAIN_PICK_CMD="sleep 5; sh $TMP/pick.sh" ISEP_DRAIN_PICK_TIMEOUT=1 run "★ pick 超過時限(Gitea 慢)→ 當成讀不到而擋,不是被砍掉後放行" 2 s10 worker.jsonl
|
||
check "逾時那次訊息是「讀不到 ≠ 沒票」" "grep -q '讀不到' '$TMP/stderr'"
|
||
|
||
echo
|
||
echo "── ④ 真的 pick(不帶假指令)接得上 ──"
|
||
unset ISEP_DRAIN_PICK_CMD
|
||
export ISEP_DRAIN_STATE_DIR="$TMP/state-real"; mkdir -p "$ISEP_DRAIN_STATE_DIR"
|
||
printf '{"session_id":"r1","transcript_path":"%s"}' "$TMP/worker.jsonl" \
|
||
| TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x ISEP_API_RETRIES=0 ISEP_MAINLINE_FILE="$TMP/none.json" \
|
||
CLAUDE_PLUGIN_ROOT="$PWD" bash "$H" >/dev/null 2>"$TMP/stderr"; got=$?
|
||
if [ "$got" = 2 ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=2 got=%s 真的 scripts/ticket pick 讀不到主線 → 當成讀不到(擋),不是當成抓盡\n' "$got"
|
||
|
||
echo
|
||
echo "通過 $PASS 條,失敗 $FAIL 條"
|
||
[ "$FAIL" = 0 ] || exit 1
|