Files
ISEP/scripts/test-ticket-pick.sh
T
Leo 2bbcda2ac3 雲端工人會分診、會組里程碑,組了就照里程碑做(inkstone/ISEP#159)
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>
2026-09-18 18:43:07 +08:00

331 lines
24 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.
#!/bin/bash
# 雲端工人抓票規則的測試(inkstone/ISEP#131
#
# 票上的驗收第 1 條:「`scripts/ticket pick` 給定主線檔,回一張符合規則的票;
# 池子裡塞一張 `Human`、一張有 assignee、一張 backlog,三張都不會被挑到(測試綠)。」
#
# 這支測四件事:
# ① `pickable()` 純函式——規則本體,一格一條,不打網路
# ② `pick` 走整條路(把 api() 換成錄音機餵一個假池子):挑到誰、順序、主線抓盡才抓逾期、
# 沒有可抓的票時離開碼 1、沒有主線時離開碼 2 且訊息講得出出路
# ③ `claim` 一個動作三件事(指派/s/doing/【身份】留言),以及該擋的擋在**打 API 之前**
# ④ 不該擋的(誤攔比漏擋更該修)
#
# 全程離線:假池子在下面的 POOL;TICKET_HOST 指到連不上的位址,任何一條真的走到網路會當場炸,
# 不會靜靜變成假綠。不開票、不改票、不留任何東西在 Gitea 上。
cd "$(dirname "$0")/.." || exit 1
T=scripts/ticket
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
export TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x
export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
export ISEP_COUNTDOWN_NOW=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,9,7,0,0,tzinfo=d.timezone.utc).timestamp()))')
unset ISEP_MAINLINE_FILE ISEP_IDENTITY CLAUDE_PROJECT_DIR ISEP_DRAIN ISEP_WORKER_LOGIN
MAIN="$TMP/mainline.json"
cat > "$MAIN" <<'JSON'
{"ref":"inkstone/Arcrun#48","owner":"inkstone","repo":"Arcrun","id":48,
"title":"主線 M","description":"測試用主線","due_on":"2026-09-12T23:59:59Z",
"open_issues":3,"closed_issues":0,"members":[],"set_at":"2026-09-07 07:30"}
JSON
# ── 假池子:三個 repo、三個 open milestone、十一張票 ─────────────────────
# 主線 M(未逾期,主線)/逾期 M(2026-08-19 到期)/未來 M2026-10-01 到期,不是主線)
cat > "$TMP/pool.py" <<'PY'
import json, os
MS = {"main": {"id": 48, "title": "主線 M", "due_on": "2026-09-12T23:59:59Z"},
"late": {"id": 49, "title": "逾期 M", "due_on": "2026-08-19T23:59:59Z"},
"future": {"id": 50, "title": "未來 M", "due_on": "2026-10-01T23:59:59Z"}}
def I(repo, n, labels, ms=None, assignees=(), created="2026-09-01T00:00:00Z", pr=False, title=None):
d = {"number": n, "title": title or f"票 {repo}#{n}", "state": "open",
"labels": [{"name": l} for l in labels],
"assignees": [{"login": a} for a in assignees],
"milestone": MS[ms] if ms else None, "created_at": created,
"repository": {"full_name": f"inkstone/{repo}"}}
if pr: d["pull_request"] = {"merged": False}
return d
POOL = {
"ISEP": [I("ISEP", 1, ["s/todo"], None), # ★ 可抓:沒里程碑,靠相依邊在主線上
I("ISEP", 13, ["s/todo"], "main"), # ISEP 裡「同名」的里程碑:不是主線(ISEP#133)
I("ISEP", 2, ["s/todo", "Human"], "main"), # 票上第 1 條:Human
I("ISEP", 3, ["s/todo"], "main", assignees=("someone",)), # 票上第 1 條:有 assignee
I("ISEP", 4, ["s/backlog"], None), # 票上第 1 條:backlog
I("ISEP", 5, ["s/todo"], None), # 沒里程碑
I("ISEP", 10, ["s/todo"], "main", pr=True), # PR 不是票
I("ISEP", 11, ["s/todo", "human/exec"], "main")], # 執行者是人
"Arcrun": [I("Arcrun", 6, ["s/todo", "p/high"], "main", created="2026-08-27T00:00:00Z"), # ★ 主線裡最優先
I("Arcrun", 7, ["s/todo"], "late"), # 逾期里程碑:主線抓盡才輪到
I("Arcrun", 8, ["s/todo"], "future")], # 不是主線也沒逾期:永遠不抓
"mira": [I("mira", 9, ["s/todo", "s/doing"], "main"), # 09-07 實查 mira#6 的形狀:有人在做
# ── 票池那一層才看得到的(inkstone/ISEP#130):都沒有里程碑 ──
I("mira", 20, ["s/triage"], None), # 還沒決定要不要做:不抓
I("mira", 21, [], None), # 沒有 s/*(還沒驗傷):不抓,但要被點名
I("mira", 22, ["s/todo"], None, assignees=("Leo",)), # 指派 Leo:不抓
I("mira", 23, ["s/backlog", "Human"], None), # Human:不抓
I("mira", 24, ["s/review"], None), # 等總管:不抓
I("mira", 25, ["s/backlog", "p/high"], None, created="2026-09-05T00:00:00Z"), # ★ 票池裡最優先
I("mira", 26, ["s/todo", "hub"], None), # hub:容器不是任務
I("mira", 27, ["Human"], None)], # 沒 s/* 但掛 Human:不抓、也不分診(ISEP#159
# 分頁:伺服器每頁上限 30(比 pick 要的 50 小)——只看「不滿一頁」就停的寫法會漏掉第 31 張之後的
"big": [I("big", n, [], None) for n in range(1, 31)] + [I("big", 31, ["s/backlog"], None)],
}
MSN = {"ISEP": ["main", "late", "future"], "Arcrun": ["main", "late", "future"], "mira": ["main"]}
SERVER_PAGE_MAX = 30
# 相依邊(inkstone/ISEP#133):主線錨在 Arcrun#48Arcrun#6 把 ISEP#1 當相依 ⇒ ISEP#1 在主線上;
# ISEP#13 掛在 ISEP 的同名里程碑但沒有任何邊 ⇒ 不在主線上。mira#9 同理(同名、沒邊)。
DEPS = {"Arcrun#6": ["ISEP#1"]}
BLOCKS = {}
for _p, _cs in DEPS.items():
for _c in _cs:
BLOCKS.setdefault(_c, []).append(_p)
def _by_ref(ref):
repo, n = ref.split("#")
return next(i for i in POOL[repo] if i["number"] == int(n))
DROP = set(json.loads(os.environ.get("DROP", "[]"))) # 測「主線抓盡」用:把某幾張從池子拿掉
def fake_api(path, payload=None, method=None):
rec.append((method or ("POST" if payload is not None else "GET"), path, payload))
if path.startswith("/orgs/inkstone/repos"):
return [{"name": r} for r in POOL]
if path == "/user":
return {"login": "claude-code"}
if "/milestones?" in path:
repo = path.split("/")[3]
return [MS[k] for k in MSN.get(repo, [])]
if "/issues?" in path:
repo = path.split("/")[3]
import urllib.parse
q = urllib.parse.parse_qs(path.split("?", 1)[1])
rows = [i for i in POOL[repo] if f"{repo}#{i['number']}" not in DROP]
if "milestones" in q:
want = q["milestones"][0]
return [i for i in rows if (i["milestone"] or {}).get("title") == want]
# 不帶里程碑=整個 repo 的 open 票(票池那一層),照 Gitea 的分頁
page = int(q.get("page", ["1"])[0]); per = min(int(q.get("limit", ["50"])[0]), SERVER_PAGE_MAX)
return rows[(page - 1) * per: page * per]
if path.endswith("/labels?limit=100"):
return [{"name": n, "id": k} for k, n in
enumerate(["s/todo", "s/doing", "s/review", "p/high", "Human"], 1)]
import re
m = re.match(r"/repos/inkstone/(\w+)/issues/(\d+)/(dependencies|blocks)(\?.*)?$", path)
if m:
table = DEPS if m.group(3) == "dependencies" else BLOCKS
return [_by_ref(r) for r in table.get(f"{m.group(1)}#{m.group(2)}", [])]
m = re.match(r"/repos/inkstone/(\w+)/issues/(\d+)$", path)
if m and (method or "GET") == "GET":
return next(i for i in POOL[m.group(1)] if i["number"] == int(m.group(2)))
return {}
rec = []
PY
# probe <python 片段> —— 把 scripts/ticket 當模組載進來、api() 換成假池子,印出片段的值
probe() {
python3 - "$1" "$2" <<'PY'
import sys, os, json, io, contextlib, importlib.util as u
from importlib.machinery import SourceFileLoader as L
spec = u.spec_from_loader("t", L("t", "scripts/ticket"))
m = u.module_from_spec(spec); spec.loader.exec_module(m)
ns = {}
exec(open(os.environ["POOL_PY"]).read(), ns)
m.api = ns["fake_api"]; rec = ns["rec"]
argv = json.loads(sys.argv[2]) if sys.argv[2] else None
buf = io.StringIO(); rc = None
if argv is not None:
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
try:
getattr(m, argv[0])(argv[1:])
except SystemExit as e:
rc = e.code
print(eval(sys.argv[1], {"m": m, "rec": rec, "rc": rc, "out": buf.getvalue(), "I": ns["I"], "json": json}))
PY
}
export POOL_PY="$TMP/pool.py"
p() { # p <說明> <want> <python 片段> [argv json]
got=$(probe "$3" "${4:-}" 2>&1)
if [ "$got" = "$2" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s %s\n' "$2" "$got" "$1"
}
echo "── ① pickable():規則本體,一格一條(純函式,不打網路)──"
p "★ s/todo+沒人+沒 Human+主線 → 可抓" "True" "m.pickable(I('ISEP',1,['s/todo'],'main'),'主線 M')[0]"
p "★ 掛 Human → 不可抓(票上第 1 條)" "False" "m.pickable(I('ISEP',2,['s/todo','Human'],'main'),'主線 M')[0]"
p "★ 有 assignee → 不可抓(票上第 1 條)" "False" "m.pickable(I('ISEP',3,['s/todo'],'main',assignees=('x',)),'主線 M')[0]"
p "★ s/backlog、沒里程碑 → 不可抓(票上第 1 條)" "False" "m.pickable(I('ISEP',4,['s/backlog'],None),'主線 M')[0]"
p "s/todo 但沒里程碑 → 不可抓(不抓沒里程碑的)" "False" "m.pickable(I('ISEP',5,['s/todo'],None),'主線 M')[0]"
p "human/exec(執行者是人)→ 不可抓" "False" "m.pickable(I('ISEP',11,['s/todo','human/exec'],'main'),'主線 M')[0]"
p "s/todos/doing 同時掛著(mira#6 的形狀)→ 不可抓,有人在做" "False" "m.pickable(I('mira',9,['s/todo','s/doing'],'main'),'主線 M')[0]"
p "hubscope 容器,09-07 實查 ISEP#31–35 的形狀)→ 不可抓" "False" "m.pickable(I('ISEP',12,['s/todo','hub'],'main'),'主線 M')[0]"
p "PR 不是票 → 不可抓" "False" "m.pickable(I('ISEP',10,['s/todo'],'main',pr=True),'主線 M')[0]"
p "里程碑不是要求的那個 → 不可抓" "False" "m.pickable(I('Arcrun',8,['s/todo'],'future'),'主線 M')[0]"
p "里程碑標題只差空白/大小寫 → 仍算同一個(norm 只有 mainline.py 那一份)" "True" "m.pickable(I('ISEP',1,['s/todo'],'main'),' 主線 m ')[0]"
p "★ 只指派給自己的 s/todo(總管 handback 回池子的 Arcrun#176 形狀)→ 可抓(ISEP#33" "True" "m.pickable(I('Arcrun',176,['s/todo'],'main',assignees=('claude-code',)),'主線 M','claude-code')[0]"
p "指派給自己+別人 → 不可抓" "False" "m.pickable(I('Arcrun',176,['s/todo'],'main',assignees=('claude-code','Leo')),'主線 M','claude-code')[0]"
p "指派給自己但 s/doing(真的有人在做)→ 不可抓" "False" "m.pickable(I('Arcrun',197,['s/doing'],'main',assignees=('claude-code',)),'主線 M','claude-code')[0]"
p "不知道自己是誰(me=None)→ 照舊不可抓,不猜" "False" "m.pickable(I('Arcrun',176,['s/todo'],'main',assignees=('claude-code',)),'主線 M')[0]"
p "指派給自己但掛 Human → 不可抓(Human 照擋)" "False" "m.pickable(I('ISEP',130,['s/todo','Human'],'main',assignees=('claude-code',)),'主線 M','claude-code')[0]"
p "理由講得出「為什麼不能抓」(不是只回 False)" "True" "'Human' in m.pickable(I('ISEP',2,['s/todo','Human'],'main'),'主線 M')[1]"
p "順序:p/high 先於沒標,沒標先於 p/low" "True" \
"m.pick_order(I('a',1,['s/todo','p/high'],'main')) < m.pick_order(I('a',2,['s/todo'],'main')) < m.pick_order(I('a',3,['s/todo','p/low'],'main'))"
p "順序:同優先序看開票時間,舊的先" "True" \
"m.pick_order(I('a',9,['s/todo'],'main',created='2026-08-01T00:00:00Z')) < m.pick_order(I('a',1,['s/todo'],'main',created='2026-09-01T00:00:00Z'))"
echo
echo "── ② pick 走整條路(假池子 11 張,三個 repo)──"
ARGS="[\"cmd_pick\",\"--mainline\",\"$MAIN\"]"
ALL="[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--all\",\"--json\"]"
p "★ 給定主線檔 → 回一張,而且是主線裡最優先的 Arcrun#6p/high" "True" "'inkstone/Arcrun#6' in out and rc is None" "$ARGS"
p "只回一張(不是整份清單)" "1" "out.count(' ● ')" "$ARGS"
p "★ --all:主線裡可抓的恰好是 Arcrun#6、ISEP#1 兩張——Humanassigneebacklog 三張都不在" \
"['inkstone/Arcrun#6', 'inkstone/ISEP#1']" "[r['ref'] for r in json.loads(out) if r['group']=='主線']" "$ALL"
p "★ ISEP#1 沒掛里程碑,是靠「Arcrun#6 把它當相依」進主線的(成員=相依邊,ISEP#133)" "True" \
"'inkstone/ISEP#1' in out and any(p.endswith('/issues/6/dependencies?limit=100') for _,p,_ in rec)" "$ALL"
p "★ ISEP#13 掛在 ISEP 裡**同名**的里程碑、沒有任何邊 → 不在主線裡(同名不算,leo 09-07)" "False" \
"'inkstone/ISEP#13' in out" "$ALL"
p "主線那一組只讀 hub 里程碑那一個 repo 的 milestone 清單(不去別的 repo 找同名)" "True" \
"not any('/repos/inkstone/ISEP/issues?' in p and '主線' in p for _,p,_ in rec)" "$ALL"
p "★ --all:逾期里程碑那張 Arcrun#7 排在主線之後(主線抓盡才輪到)" \
"['inkstone/Arcrun#7']" "[r['ref'] for r in json.loads(out) if r['group']!='主線']" "$ALL"
p "沒逾期也不是主線的 Arcrun#8 不在清單裡" "False" "'inkstone/Arcrun#8' in out" "$ALL"
p "backlogISEP#4)/沒里程碑(ISEP#5)不在清單裡" "False" "'ISEP#4' in out or 'ISEP#5' in out" "$ALL"
p "沒有任何一條真的走到網路(api 全被錄音機接住)" "True" "all(p.startswith('/') for _,p,_ in rec)" "$ALL"
p "pick 只讀不寫(沒有 POSTPATCHPUT" "True" "all(mm=='GET' for mm,_,_ in rec)" "$ALL"
p "訊息印出下一步(認領那一行用絕對路徑,貼到別的 cwd 也叫得到正本)" "True" "'claim inkstone/Arcrun#6' in out and m.self_path() in out" "$ARGS"
echo
echo " (主線抓盡 → 才輪到逾期里程碑)"
export DROP='["Arcrun#6","ISEP#1"]'
p "★ 主線兩張都被拿走後 → 回逾期里程碑的 Arcrun#7" "True" "'inkstone/Arcrun#7' in out and rc is None" "$ARGS"
export DROP='["Arcrun#6","ISEP#1","Arcrun#7"]'
p "★ 主線與逾期都抓盡 → 離開碼 1(真的沒有,不是故障)" "1" "rc" "$ARGS"
p "沒帶 --pool 時訊息明講「這一趟沒有看票池」並給出 --pool 這條路(ISEP#130:不再說成正常收工)" "True" \
"'沒有看票池' in out and '--pool' in out and '不是故障' not in out" "$ARGS"
p "沒帶 --pool、主線與逾期都抓盡 → 票池一通都沒讀(本機總管不會因此被點名去抓票池)" "False" \
"any('page=' in p for _,p,_ in rec)" "$ARGS"
p "抓盡時**沒有**回 Arcrun#8(未來里程碑)" "False" "'Arcrun#8' in out" "$ARGS"
unset DROP
echo
echo " (沒有主線 ≠ 去抓別的)"
NOML="[\"cmd_pick\",\"--mainline\",\"$TMP/none.json\"]"
p "主線檔讀不到 → 離開碼 2(讀不到 ≠ 沒有)" "2" "rc" "$NOML"
p "…而且一通 API 都沒打" "0" "len(rec)" "$NOML"
p "沒給 --mainline、狀態檔也沒有、cwd 也沒有 → 離開碼 2,訊息給出兩條出路" "True" \
"rc==2 and 'mainline set' in out and 'system-dev/mainline.json' in out" "[\"cmd_pick\"]"
echo
echo "── ③ claim:一個動作三件事 ──"
CL="[\"cmd_claim\",\"inkstone/Arcrun#6\",\"--mainline\",\"$MAIN\",\"--name\",\"isep-hand\"]"
p "★ 指派給本 token 的使用者(欄位,不是一句話)" "[['claude-code']]" \
"[pl['assignees'] for mm,p,pl in rec if mm=='PATCH' and p=='/repos/inkstone/Arcrun/issues/6']" "$CL"
p "★ s/todo 換成 s/doingp/high 留著(不是整組蓋掉)" "True" \
"[sorted(pl['labels'])==[2,4] for mm,p,pl in rec if mm=='PUT' and p.endswith('/issues/6/labels')]==[True]" "$CL"
p "★ 留言第一行是【身份】isep-handinkstone/Arcrun-" "True" \
"[pl['body'].split(chr(10))[0] for mm,p,pl in rec if mm=='POST' and p.endswith('/issues/6/comments')]==['【身份】isep-handinkstone/Arcrun-']" "$CL"
p "留言講了做完要走 handback(不推 main、不出貨)" "True" \
"any('handback' in pl['body'] and '不推 main' in pl['body'] for mm,p,pl in rec if mm=='POST' and p.endswith('/comments'))" "$CL"
p "三件事都做了,離開碼 0" "None" "rc" "$CL"
p "--as 指定登入名時用它" "[['cloud-bot']]" \
"[pl['assignees'] for mm,p,pl in rec if mm=='PATCH']" "[\"cmd_claim\",\"inkstone/Arcrun#6\",\"--mainline\",\"$MAIN\",\"--as\",\"cloud-bot\"]"
p "逾期里程碑的票(Arcrun#7)也認領得了——主線抓盡時輪到它" "None" "rc" \
"[\"cmd_claim\",\"inkstone/Arcrun#7\",\"--mainline\",\"$MAIN\"]"
p "pick --claim:挑到誰就認領誰(Arcrun#6" "True" \
"any(mm=='PATCH' and p.endswith('/issues/6') for mm,p,pl in rec)" "[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--claim\",\"--name\",\"isep-hand\"]"
echo
echo " (該擋的:規則在 claim 這一端再驗一次,擋在**寫入之前**)"
blocked() { # blocked <說明> <ref> [extra argv...]
what=$1; ref=$2; shift 2
extra=$(printf '%s\n' "$@" | python3 -c "import sys,json;print(','.join(json.dumps(l.rstrip()) for l in sys.stdin if l.strip()))")
argv="[\"cmd_claim\",\"$ref\",\"--mainline\",\"$MAIN\"${extra:+,$extra}]"
p "$what → 離開碼 2,且沒有任何寫入" "True" \
"rc==2 and not any(mm in ('PATCH','PUT','POST') for mm,_,_ in rec)" "$argv"
}
blocked "掛 Human 的 ISEP#2" inkstone/ISEP#2
blocked "已指派的 ISEP#3" inkstone/ISEP#3
blocked "backlog 的 ISEP#4" inkstone/ISEP#4
blocked "沒逾期也不是主線的 Arcrun#8" inkstone/Arcrun#8
blocked "有人在做的 mira#9s/doing 也掛著)" inkstone/mira#9
blocked "掛在 ISEP 同名里程碑、沒有相依邊的 ISEP#13(同名不是主線)" inkstone/ISEP#13
p "claim 靠相依進主線的 ISEP#1(沒里程碑)→ 過(留言寫「靠相依在主線上」)" "True" \
"rc is None and any('靠相依' in pl['body'] for mm,p,pl in rec if mm=='POST' and p.endswith('/comments'))" \
"[\"cmd_claim\",\"inkstone/ISEP#1\",\"--mainline\",\"$MAIN\"]"
p "擋下時訊息講得出理由與出路(pick" "True" "'Human' in out and 'pick' in out" \
"[\"cmd_claim\",\"inkstone/ISEP#2\",\"--mainline\",\"$MAIN\"]"
p "身份名不在名單上 → 在打任何 API 之前就擋(一通都沒打)" "True" "rc==2 and len(rec)==0" \
"[\"cmd_claim\",\"inkstone/Arcrun#6\",\"--mainline\",\"$MAIN\",\"--name\",\"路人\"]"
echo
echo "── ⑤ 票池那一層(inkstone/ISEP#130 c7801:幾百張 open 票,工人卻回「沒事做」)──"
POOLA="[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\"]"
POOLALL="[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\",\"--all\",\"--json\"]"
p "pickables/backlog 在票池那一層可抓" "True" "m.pickable(I('ISEP',4,['s/backlog'],None),None,None,m.POOL_STATUSES)[0]"
p "pickables/triage 在票池那一層仍不可抓(要不要做還沒決定)" "False" "m.pickable(I('x',1,['s/triage'],None),None,None,m.POOL_STATUSES)[0]"
p "pickable:沒有 s/* 仍不可抓,理由講得出「沒有 s/*」" "True" "'沒有 s/*' in m.pickable(I('x',1,[],None),None,None,m.POOL_STATUSES)[1]"
p "pickable:票池裡的 backlog 掛 Human → 不可抓(不該碰的一格都沒放寬)" "False" "m.pickable(I('x',1,['s/backlog','Human'],None),None,None,m.POOL_STATUSES)[0]"
p "pickable:票池裡指派給 Leo → 不可抓" "False" "m.pickable(I('x',1,['s/todo'],None,assignees=('Leo',)),None,'claude-code',m.POOL_STATUSES)[0]"
p "順序:同優先序 s/todo 先於 s/backlog" "True" \
"m.pick_order(I('a',9,['s/todo'],None)) < m.pick_order(I('a',1,['s/backlog'],None))"
p "主線有票時帶 --pool 也照樣先回主線的 Arcrun#6,票池一通都沒讀(能省就省)" "True" \
"'inkstone/Arcrun#6' in out and not any('page=' in p for _,p,_ in rec)" "$POOLA"
export DROP='["Arcrun#6","ISEP#1","Arcrun#7"]'
p "★ 主線與逾期都抓盡、帶 --pool → 回票池裡最優先的 mira#25p/high 的 backlog" "True" \
"'inkstone/mira#25' in out and rc is None" "$POOLA"
p "★ --all:票池可抓的恰好是這幾張,照 p/high → s/todo → s/backlog 排(Human/指派 Leotriage/沒 s/*reviews/doinghubPR 都不在)" \
"['inkstone/mira#25', 'inkstone/ISEP#5', 'inkstone/Arcrun#8', 'inkstone/ISEP#13', 'inkstone/ISEP#4', 'inkstone/big#31']" \
"[r['ref'] for r in json.loads(out) if r['group']=='票池']" "$POOLALL"
p "★ 分頁:伺服器每頁只給 30 張時,第 31 張(big#31)照樣讀得到" "True" "'inkstone/big#31' in out" "$POOLALL"
p "票池那一層也只讀不寫" "True" "all(mm=='GET' for mm,_,_ in rec)" "$POOLALL"
export ISEP_DRAIN=1
p "★ 環境 ISEP_DRAIN=1(雲端工人的環境)= 帶 --pool,指令不必改" "True" \
"'inkstone/mira#25' in out and rc==0" "[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--json\"]"
unset ISEP_DRAIN
unset DROP
export DROP='["Arcrun#6","ISEP#1","Arcrun#7","mira#25","Arcrun#8","ISEP#13","ISEP#5","ISEP#4","big#31"]'
p "★ 票池抓盡、但還有沒 s/* 的票 → 回一張待分診的(同一天開的,票號序第一的 big#1),下一步是 triage 不是 claimISEP#159" "True" \
"rc is None and 'inkstone/big#1' in out and ' triage inkstone/big#1 ' in out and 'claim inkstone/big#1' not in out" "$POOLA"
p "--json:待分診那一列帶 group=待分診 與貼了就能跑的 next(佇列閘照它印)" "True" \
"(lambda r: r['group']=='待分診' and ' triage ' in r['next'])(json.loads(out))" \
"[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\",\"--json\"]"
p "待分診那一層不回掛 Human 的 mira#27(沒 s/* 也不碰)" "False" "'mira#27' in out" "$POOLALL"
p "pick --claim 遇到待分診的票 → 不認領(沒有任何寫入)" "True" \
"not any(mm in ('PATCH','PUT','POST') for mm,_,_ in rec)" \
"[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\",\"--claim\"]"
export DROP='["Arcrun#6","ISEP#1","Arcrun#7","mira#25","Arcrun#8","ISEP#13","ISEP#5","ISEP#4","big#31","mira#21"'"$(python3 -c 'print("".join(",\"big#%d\"" % n for n in range(1,31)))')"']'
p "★ 票池與待分診都抓盡 → 離開碼 1" "1" "rc" "$POOLA"
p "★ 可抓 0 張時列出池子大小與「不能抓的理由」各幾張(沒 s/* 又掛 Human 的被點名),而且不說成「正常/不是故障」" "True" \
"'open 票池' in out and '沒有 s/*' in out and '不是故障' not in out and '正常' not in out" "$POOLA"
p "--json 抓盡時同一張理由表寫到 stderr(stdout 仍是 null,佇列閘照舊解析得動)" "True" \
"out.startswith('null') and '不能抓' not in out.split(chr(10))[0] and '沒有 s/*' in out" \
"[\"cmd_pick\",\"--mainline\",\"$MAIN\",\"--pool\",\"--json\"]"
unset DROP
echo
echo " (認領票池裡的票)"
CLP="[\"cmd_claim\",\"inkstone/ISEP#4\",\"--mainline\",\"$MAIN\",\"--name\",\"isep-hand\",\"--pool\"]"
p "★ --pool 認領 backlog 的 ISEP#4 → 過,s/backlog 換成 s/doing" "True" \
"rc is None and [sorted(pl['labels']) for mm,p,pl in rec if mm=='PUT' and p.endswith('/issues/4/labels')]==[[2]]" "$CLP"
p "認領留言寫明是從 open 票池抓的、狀態從 s/backlog 變 s/doing" "True" \
"any('open 票池' in pl['body'] and 's/backlog → s/doing' in pl['body'] for mm,p,pl in rec if mm=='POST' and p.endswith('/comments'))" "$CLP"
blocked "--pool 也擋掛 Human 的 mira#23" inkstone/mira#23 --pool
blocked "--pool 也擋指派 Leo 的 mira#22" inkstone/mira#22 --pool
blocked "--pool 也擋 s/triage 的 mira#20" inkstone/mira#20 --pool
blocked "--pool 也擋沒有 s/* 的 mira#21" inkstone/mira#21 --pool
echo
echo "── ④ 動詞接得上(不帶假池子,走到網路才炸)──"
out=$(python3 "$T" pick --mainline "$MAIN" 2>&1); got=$?
if [ "$got" = 2 ] && printf '%s' "$out" | grep -q '拿不到 Gitea'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=2 got=%s pick 連不上 Gitea → 離開碼 2 並說「讀不到 ≠ 沒有」(不是離開碼 1 的假「沒票」)\n' "$got"
out=$(python3 "$T" 2>&1)
if printf '%s' "$out" | grep -q "ticket pick" && printf '%s' "$out" | grep -q "ticket claim"; then PASS=$((PASS+1)); printf ' ✅ '
else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf '不帶參數的說明裡列得出 pick/claim(撈不到的東西等於不存在)\n'
out=$(python3 "$T" usage pick 2>&1); got=$?
if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--mainline'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'ticket usage pick 印得出用法\n'
echo
echo "通過 $PASS 條,失敗 $FAIL"
[ "$FAIL" = 0 ] || exit 1