#!/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 到期)/未來 M(2026-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#48,Arcrun#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 —— 把 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 <說明> [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/todo+s/doing 同時掛著(mira#6 的形狀)→ 不可抓,有人在做" "False" "m.pickable(I('mira',9,['s/todo','s/doing'],'main'),'主線 M')[0]" p "hub(scope 容器,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#6(p/high)" "True" "'inkstone/Arcrun#6' in out and rc is None" "$ARGS" p "只回一張(不是整份清單)" "1" "out.count(' ● ')" "$ARGS" p "★ --all:主線裡可抓的恰好是 Arcrun#6、ISEP#1 兩張——Human/assignee/backlog 三張都不在" \ "['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 "backlog(ISEP#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 只讀不寫(沒有 POST/PATCH/PUT)" "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/doing,p/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-hand/inkstone/Arcrun/-" "True" \ "[pl['body'].split(chr(10))[0] for mm,p,pl in rec if mm=='POST' and p.endswith('/issues/6/comments')]==['【身份】isep-hand/inkstone/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 <說明> [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#9(s/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 "pickable:s/backlog 在票池那一層可抓" "True" "m.pickable(I('ISEP',4,['s/backlog'],None),None,None,m.POOL_STATUSES)[0]" p "pickable:s/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#25(p/high 的 backlog)" "True" \ "'inkstone/mira#25' in out and rc is None" "$POOLA" p "★ --all:票池可抓的恰好是這幾張,照 p/high → s/todo → s/backlog 排(Human/指派 Leo/triage/沒 s/*/review/s/doing/hub/PR 都不在)" \ "['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 不是 claim(ISEP#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