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>
301 lines
22 KiB
Bash
Executable File
301 lines
22 KiB
Bash
Executable File
#!/bin/bash
|
||
# 雲端工人「分診+組里程碑」的測試(inkstone/ISEP#159)
|
||
#
|
||
# 票上的驗收:
|
||
# 1. 沒狀態的票數下降,每張被動過的票上都有一則說明(分到哪、為什麼;重複的指向哪張)
|
||
# 2. 提出新里程碑:載體票在 InkStoneCo、執行票掛成前置、名字不是版本號、有期限、deliverable=版本
|
||
# 3. 不碰 Human、不碰指派給 Leo 的票
|
||
# 4. 組了之後照里程碑往下做(pick --pool 先回它的前置票)
|
||
#
|
||
# 全程離線:api() 換成錄音機餵假池子;TICKET_HOST 指到連不上的位址,漏接一條會當場炸,不會假綠。
|
||
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 ISEP_MEMBER_BACKOFF=0
|
||
export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
|
||
# 時鐘定在 2026-09-18 00:00 UTC
|
||
export ISEP_COUNTDOWN_NOW=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,9,18,0,0,tzinfo=d.timezone.utc).timestamp()))')
|
||
unset ISEP_MAINLINE_FILE ISEP_IDENTITY CLAUDE_PROJECT_DIR ISEP_DRAIN
|
||
export ISEP_WORKER_LOGIN=claude-code # 抓票者自己是誰(真跑時問 /user;離線測試不打網路)
|
||
|
||
MAIN="$TMP/mainline.json"
|
||
cat > "$MAIN" <<'JSON'
|
||
{"ref":"inkstone/InkStoneCo#65","owner":"inkstone","repo":"InkStoneCo","id":65,
|
||
"title":"主線 M","due_on":"2026-09-25T23:59:59Z","members":[]}
|
||
JSON
|
||
|
||
cat > "$TMP/pool.py" <<'PY'
|
||
import json, os, re, urllib.parse
|
||
MS = {"main": {"id": 65, "title": "主線 M", "due_on": "2026-09-25T23:59:59Z", "state": "open"},
|
||
"mine": {"id": 70, "title": "組好的 M", "due_on": "2026-09-30T23:59:59Z", "state": "open"},
|
||
"sub": {"id": 9, "title": "子 repo 的舊里程碑", "due_on": "2026-10-30T23:59:59Z", "state": "open"}}
|
||
def I(repo, n, labels, ms=None, assignees=(), created="2026-09-01T00:00:00Z", pr=False, state="open", title=None):
|
||
d = {"number": n, "title": title or f"票 {repo}#{n}", "state": state,
|
||
"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 = {
|
||
"InkStoneCo": [I("InkStoneCo", 55, [], None, title="身為一天只看兩眼的人…"), # ★ 載體候選:沒里程碑、沒人
|
||
I("InkStoneCo", 132, ["Human", "s/pending"], "main", assignees=("Leo",)), # 主線載體(Leo 的)
|
||
I("InkStoneCo", 80, ["s/backlog"], "mine"), # 組好的 M 的載體(有 s/* 也不是任務)
|
||
I("InkStoneCo", 81, ["Human"], None), # 掛 Human 的載體候選
|
||
I("InkStoneCo", 82, [], "main"),
|
||
I("InkStoneCo", 83, [], None, assignees=("Leo",)), # 指派 Leo 的載體候選
|
||
I("InkStoneCo", 84, ["s/backlog"], None)], # 已是「組好的 M」載體的前置——不能再當載體 # 已掛在主線里程碑的載體候選
|
||
"ISEP": [I("ISEP", 1, [], None, created="2026-08-01T00:00:00Z"), # ★ 待分診(最舊)
|
||
I("ISEP", 2, [], None), # 待分診
|
||
I("ISEP", 3, ["Human"], None), # 沒 s/*、掛 Human:不碰
|
||
I("ISEP", 4, [], None, assignees=("Leo",)), # 沒 s/*、指派 Leo:不碰
|
||
I("ISEP", 5, ["s/backlog", "type/bug"], None), # ★ 已分診:可組進里程碑
|
||
I("ISEP", 6, ["s/triage"], None), # 還沒決定:不可組
|
||
I("ISEP", 7, ["s/backlog"], None), # 已是組好的 M 的前置
|
||
I("ISEP", 8, ["s/backlog"], "sub"), # 掛著子 repo 里程碑
|
||
I("ISEP", 9, [], None, pr=True), # PR
|
||
I("ISEP", 10, [], None, assignees=("claude-code",))], # 只指派給自己:可分診
|
||
"Arcrun": [I("Arcrun", 20, ["s/todo"], None), # ★ 已分診:可組進里程碑
|
||
I("Arcrun", 21, ["s/todo"], None), # 組好的 M 的前置(pick 用)
|
||
I("Arcrun", 22, ["s/backlog", "Human"], None), # Human 前置:不碰
|
||
I("Arcrun", 23, ["s/backlog"], None, assignees=("Leo",))], # 指派 Leo 的前置:不碰 # Human 前置:不碰
|
||
}
|
||
MSN = {"InkStoneCo": ["main", "mine"], "ISEP": ["sub"]}
|
||
DEPS = {"InkStoneCo#80": ["ISEP#7", "Arcrun#21", "InkStoneCo#84"]}
|
||
def _by_ref(ref):
|
||
repo, n = ref.split("#")
|
||
return next(i for i in POOL[repo] if i["number"] == int(n))
|
||
BLOCKS = {}
|
||
for _p, _cs in DEPS.items():
|
||
for _c in _cs:
|
||
BLOCKS.setdefault(_c, []).append(_p)
|
||
LABELS = ["s/triage", "s/backlog", "s/todo", "s/doing", "s/review", "s/pending", "close/duplicate",
|
||
"Human", "type/bug", "p/high"]
|
||
DROP = set(json.loads(os.environ.get("DROP", "[]")))
|
||
rec = []
|
||
def fake_api(path, payload=None, method=None):
|
||
verb = method or ("POST" if payload is not None else "GET")
|
||
rec.append((verb, path, payload))
|
||
if path.startswith("/orgs/inkstone/repos"):
|
||
return [{"name": r} for r in POOL]
|
||
if path == "/user":
|
||
return {"login": "claude-code"}
|
||
if path.endswith("/labels?limit=100"):
|
||
return [{"name": n, "id": k} for k, n in enumerate(LABELS, 1)]
|
||
if "/milestones" in path:
|
||
repo = path.split("/")[3]
|
||
if verb == "POST":
|
||
return {"id": 71, "title": payload["title"]}
|
||
return [MS[k] for k in MSN.get(repo, [])]
|
||
if "/issues?" in path:
|
||
repo = path.split("/")[3]
|
||
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]
|
||
rows = [i for i in rows if i["state"] == "open"]
|
||
page = int(q.get("page", ["1"])[0]); per = int(q.get("limit", ["50"])[0])
|
||
return rows[(page - 1) * per: page * per]
|
||
m = re.match(r"/repos/inkstone/([\w-]+)/issues/(\d+)/(dependencies|blocks)(\?.*)?$", path)
|
||
if m:
|
||
if verb == "POST":
|
||
return {}
|
||
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 verb == "GET":
|
||
return _by_ref(f"{m.group(1)}#{m.group(2)}")
|
||
return {}
|
||
PY
|
||
export POOL_PY="$TMP/pool.py"
|
||
|
||
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
|
||
W = [(mm, p, pl) for mm, p, pl in rec if mm in ("POST", "PATCH", "PUT", "DELETE")]
|
||
print(eval(sys.argv[1], {"m": m, "rec": rec, "W": W, "rc": rc, "out": buf.getvalue(), "I": ns["I"], "json": json}))
|
||
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"
|
||
}
|
||
j() { python3 -c 'import json,sys;print(json.dumps(sys.argv[1:],ensure_ascii=False))' "$@"; }
|
||
|
||
echo "── ① 規則本體(純函式)──"
|
||
p "沒 s/*、沒人、沒 Human → 待分診" "True" "m.triageable(I('ISEP',1,[]),'claude-code')[0]"
|
||
p "★ 掛 Human → 不碰(驗收 3)" "False" "m.triageable(I('ISEP',3,['Human']),'claude-code')[0]"
|
||
p "human/exec 同組 → 不碰" "False" "m.triageable(I('ISEP',3,['human/exec']),'claude-code')[0]"
|
||
p "★ 指派給 Leo → 不碰(驗收 3)" "False" "m.triageable(I('ISEP',4,[],assignees=('Leo',)),'claude-code')[0]"
|
||
p "只指派給自己 → 可分診" "True" "m.triageable(I('ISEP',10,[],assignees=('claude-code',)),'claude-code')[0]"
|
||
p "不知道自己是誰、票有指派 → 不碰(不猜)" "False" "m.triageable(I('ISEP',10,[],assignees=('claude-code',)),None)[0]"
|
||
p "已經有 s/*(連 s/triage 也算)→ 不是待分診" "False" "m.triageable(I('ISEP',6,['s/triage']),'claude-code')[0]"
|
||
p "PR → 不是票" "False" "m.triageable(I('ISEP',9,[],pr=True),'claude-code')[0]"
|
||
p "已關 → 不碰" "False" "m.triageable(I('ISEP',1,[],state='closed'),'claude-code')[0]"
|
||
p "InkStoneCo 裡掛在 open 里程碑的票(載體)→ 不是待分診(它是容器)" "False" "m.triageable(I('InkStoneCo',82,[],'main'),'claude-code')[0]"
|
||
p "別的 repo 掛著里程碑、沒 s/* → 照樣待分診(載體只認 InkStoneCo)" "True" "m.triageable(I('ISEP',8,[],'sub'),'claude-code')[0]"
|
||
p "名字以版本號開頭(1.4.52 雲端零件包)→ 算版本號名字" "True" "m._version_like('1.4.52 雲端零件包')"
|
||
p "v 開頭也算" "True" "m._version_like('v0.30 什麼')"
|
||
p "名字裡提到版本但不是開頭(封測不再斷,含 1.4 修正)→ 不算(誤攔比漏擋嚴重)" "False" "m._version_like('封測不再斷,含 1.4 修正')"
|
||
|
||
echo
|
||
echo "── ② triage:一個動作=說明+欄位(驗收 1)──"
|
||
TR=$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why "還在用、要修" --name isep-hand)
|
||
p "★ 先留說明、再改標籤:寫入恰好是 [留言, 改標籤]" "[('POST', '/repos/inkstone/ISEP/issues/1/comments'), ('PUT', '/repos/inkstone/ISEP/issues/1/labels')]" \
|
||
"[(mm,p) for mm,p,_ in W]" "$TR"
|
||
p "★ 說明第一行是【身份】,內文講得出分到哪、為什麼" "True" \
|
||
"(lambda b: b.split(chr(10))[0]=='【身份】isep-hand/inkstone/ISEP/-' and 's/backlog' in b and '還在用、要修' in b)(W[0][2]['body'])" "$TR"
|
||
p "標籤換成 s/backlog(id 2)" "[2]" "W[1][2]['labels']" "$TR"
|
||
p "離開碼 0" "None" "rc" "$TR"
|
||
TD=$(j cmd_triage inkstone/ISEP#2 --dup-of inkstone/ISEP#5 --why "同一件事" --name isep-hand)
|
||
p "★ 重複:留說明→標 close/duplicate→關→在留下的那張留一則指回來" \
|
||
"[('POST', '/repos/inkstone/ISEP/issues/2/comments'), ('PUT', '/repos/inkstone/ISEP/issues/2/labels'), ('PATCH', '/repos/inkstone/ISEP/issues/2'), ('POST', '/repos/inkstone/ISEP/issues/5/comments')]" \
|
||
"[(mm,p) for mm,p,_ in W]" "$TD"
|
||
p "★ 重複那則說明指向留下的那張(驗收 1:重複的指向哪張)" "True" "'inkstone/ISEP#5' in W[0][2]['body']" "$TD"
|
||
p "關的是 state=closed、標的是 close/duplicate(id 7)" "True" "W[2][2]=={'state':'closed'} and W[1][2]['labels']==[7]" "$TD"
|
||
TDL=$(j cmd_triage inkstone/ISEP#2 --dup-of inkstone/InkStoneCo#132 --why "同一件事")
|
||
p "★ 重複的對象是 Leo 的票 → 這張照關,但**不在 Leo 的票上寫任何字**(驗收 3)" "True" \
|
||
"rc is None and not any('/InkStoneCo/' in p for _,p,_ in W) and '沒在' in out" "$TDL"
|
||
p "只指派給自己的 ISEP#10 → 分診得了" "None" "rc" "$(j cmd_triage inkstone/ISEP#10 --to s/triage --why 要總管看)"
|
||
|
||
echo
|
||
echo " (該擋的:擋下時一筆都沒寫)"
|
||
nb() { # nb <說明> <argv json> [要在訊息裡看到的字]
|
||
p "$1 → 離開碼 2、零寫入${3:+、訊息有「$3」}" "True" "rc==2 and not W${3:+ and '$3' in out}" "$2"
|
||
}
|
||
nb "★ 掛 Human 的 ISEP#3(驗收 3)" "$(j cmd_triage inkstone/ISEP#3 --to s/backlog --why x)" "掛著 Human"
|
||
nb "★ 指派給 Leo 的 ISEP#4(驗收 3)" "$(j cmd_triage inkstone/ISEP#4 --to s/backlog --why x)" "指派給 Leo"
|
||
nb "已分診過的 ISEP#6" "$(j cmd_triage inkstone/ISEP#6 --to s/backlog --why x)" "已經分診過"
|
||
nb "PR" "$(j cmd_triage inkstone/ISEP#9 --to s/backlog --why x)"
|
||
p "triage --sign cloud-worker ⇒ 說明內文署 [cloud-worker]" "True" "'[cloud-worker]' in W[0][2]['body']" \
|
||
"$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why x --sign cloud-worker)"
|
||
nb "沒寫 --why(說明是驗收 1 的本體)" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog)" "--why"
|
||
nb "--to s/todo(要經 compose 排進里程碑才給)" "$(j cmd_triage inkstone/ISEP#1 --to s/todo --why x)" "compose"
|
||
nb "--to 與 --dup-of 都給" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --dup-of inkstone/ISEP#5 --why x)"
|
||
nb "--dup-of 指到自己" "$(j cmd_triage inkstone/ISEP#1 --dup-of inkstone/ISEP#1 --why x)"
|
||
p "參數錯(沒 --why)→ 一通 API 都沒打" "0" "len(rec)" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog)"
|
||
p "身份名不在名單上 → 一通 API 都沒打" "True" "rc==2 and len(rec)==0" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why x --name 路人)"
|
||
|
||
echo
|
||
echo " (--list:工人拿什麼來判斷)"
|
||
p "★ --list 列出待分診的票(InkStoneCo 還沒分診的也算),不列 Human/Leo/已分診/PR" \
|
||
"['inkstone/ISEP#1', 'inkstone/ISEP#10', 'inkstone/ISEP#2', 'inkstone/InkStoneCo#55']" \
|
||
"[r['ref'] for r in json.loads(out)]" "$(j cmd_triage --list --json)"
|
||
p "--list --all 連已分診的 open 票也列(找重複要看全部),並標出哪些可分診" "True" \
|
||
"(lambda rows: len(rows)>10 and {r['ref'] for r in rows if r['triageable']}=={'inkstone/ISEP#1','inkstone/ISEP#2','inkstone/ISEP#10','inkstone/InkStoneCo#55'})(json.loads(out))" \
|
||
"$(j cmd_triage --list --all --json)"
|
||
p "--list 文字版講得出「不碰」的有幾張,並給出下一步那一行" "True" \
|
||
"'不碰' in out and ' triage inkstone/ISEP#1 ' in out" "$(j cmd_triage --list)"
|
||
p "--list 只讀不寫" "True" "not W" "$(j cmd_triage --list)"
|
||
|
||
echo
|
||
echo "── ③ compose:組里程碑(驗收 2)──"
|
||
CP=$(j cmd_compose --title "雲端自己把票池消化成版本" --goal "leo 不必一張張叫它做" --due 2026-10-01 \
|
||
--carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 inkstone/Arcrun#20 --name isep-hand)
|
||
p "★ 里程碑建在 inkstone/InkStoneCo(開工規約第 7 條)" "True" \
|
||
"[p for mm,p,_ in W if mm=='POST' and p.endswith('/milestones')]==['/repos/inkstone/InkStoneCo/milestones']" "$CP"
|
||
p "★ 有期限(23:59:59Z),不是 9999" "2026-10-01T23:59:59Z" \
|
||
"[pl['due_on'] for mm,p,pl in W if p.endswith('/milestones')][0]" "$CP"
|
||
p "★ 描述寫明這一輪要讓什麼變成真的、deliverable=總管驗過的新版本、載體與前置" "True" \
|
||
"(lambda d: '這一輪要讓什麼變成真的:leo 不必' in d and 'deliverable=' in d and '新版本' in d and 'inkstone/InkStoneCo#55' in d and 'inkstone/ISEP#5' in d)([pl['description'] for mm,p,pl in W if p.endswith('/milestones')][0])" "$CP"
|
||
p "★ 載體票掛上新里程碑(id 71)" "True" \
|
||
"('PATCH','/repos/inkstone/InkStoneCo/issues/55',{'milestone':71}) in W" "$CP"
|
||
p "★ 兩張執行票都掛成載體票的前置(/dependencies)" "True" \
|
||
"[pl for mm,p,pl in W if mm=='POST' and p=='/repos/inkstone/InkStoneCo/issues/55/dependencies']==[{'owner':'inkstone','repo':'ISEP','index':5},{'owner':'inkstone','repo':'Arcrun','index':20}]" "$CP"
|
||
p "★ s/backlog → s/todo(已排進里程碑,等人領票),type/bug 留著" "[3, 9]" \
|
||
"sorted([pl['labels'] for mm,p,pl in W if mm=='PUT' and p=='/repos/inkstone/ISEP/issues/5/labels'][0])" "$CP"
|
||
p "本來就 s/todo 的 Arcrun#20 不重貼標籤" "False" "any(p.endswith('/Arcrun/issues/20/labels') for _,p,_ in W)" "$CP"
|
||
p "每張前置與載體都留了一則說明(第一行【身份】)" "True" \
|
||
"sorted(p for mm,p,pl in W if mm=='POST' and p.endswith('/comments') and pl['body'].startswith('【身份】'))==['/repos/inkstone/Arcrun/issues/20/comments','/repos/inkstone/ISEP/issues/5/comments','/repos/inkstone/InkStoneCo/issues/55/comments']" "$CP"
|
||
p "★ 不開任何新票(開工規約第 2 條)" "False" \
|
||
"any(mm=='POST' and p.endswith('/issues') for mm,p,_ in W)" "$CP"
|
||
p "離開碼 0,印出里程碑網址" "True" "rc is None and '/inkstone/InkStoneCo/milestone/71' in out" "$CP"
|
||
CPS=$(j cmd_compose --title "雲端自己把票池消化成版本" --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 --sign cloud-worker)
|
||
p "--sign cloud-worker ⇒ 每則留言內文都署 [cloud-worker];--sign 的值不會被當成前置票" "True" \
|
||
"rc is None and all('[cloud-worker]' in pl['body'] for mm,p,pl in W if p.endswith('/comments')) and not any('cloud-worker' in str(pl) for mm,p,pl in W if p.endswith('/dependencies'))" "$CPS"
|
||
p "沒給 --sign ⇒ 不署一個寫死的名字(誰跑的就署誰)" "False" \
|
||
"any('[cloud-worker]' in pl['body'] for mm,p,pl in W if p.endswith('/comments'))" "$CP"
|
||
|
||
echo
|
||
echo " (該擋的:先全部驗過才寫——擋下時一筆都沒寫,而且理由一次列齊)"
|
||
cb() { # cb <說明> <要在訊息裡看到的字> <argv...>
|
||
what=$1; want=$2; shift 2
|
||
p "$what → 離開碼 2、零寫入、訊息有「$want」" "True" "rc==2 and not W and '$want' in out" "$(j cmd_compose "$@")"
|
||
}
|
||
OK=(--goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#55)
|
||
cb "★ 名字是版本號(開工規約第 5 條)" "版本號" --title "1.4.52 雲端零件包" "${OK[@]}" inkstone/ISEP#5
|
||
cb "沒名字" "--title" "${OK[@]}" inkstone/ISEP#5
|
||
cb "★ 沒期限(開工規約第 6 條)" "--due" --title 好名字 --goal g --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5
|
||
cb "期限 9999" "天內" --title 好名字 --goal g --due 9999-01-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5
|
||
cb "期限在過去" "天內" --title 好名字 --goal g --due 2026-09-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5
|
||
cb "載體不在 InkStoneCo(不准在子 repo 建里程碑)" "InkStoneCo" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/ISEP#1 inkstone/ISEP#5
|
||
cb "沒有前置" "前置" --title 好名字 "${OK[@]}"
|
||
cb "★ 載體掛 Human(驗收 3)" "載體 inkstone/InkStoneCo#81:掛著 Human" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#81 inkstone/ISEP#5
|
||
cb "★ 載體指派給 Leo(驗收 3)" "載體 inkstone/InkStoneCo#83:指派給 Leo" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#83 inkstone/ISEP#5
|
||
cb "載體本身是別的 open 里程碑載體的前置(那一輪還在跑)" "那一輪還在跑" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#84 inkstone/ISEP#5
|
||
cb "載體已在別的 open 里程碑" "已經掛在里程碑" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#82 inkstone/ISEP#5
|
||
cb "同名 open 里程碑已在" "同名" --title "組好的 M" "${OK[@]}" inkstone/ISEP#5
|
||
cb "★ 前置掛 Human(驗收 3)" "前置 inkstone/Arcrun#22:掛著 Human" --title 好名字 "${OK[@]}" inkstone/Arcrun#22
|
||
cb "★ 前置指派給 Leo(驗收 3)" "前置 inkstone/Arcrun#23:指派給 Leo" --title 好名字 "${OK[@]}" inkstone/Arcrun#23
|
||
cb "前置還沒分診(沒 s/*)" "還沒分診" --title 好名字 "${OK[@]}" inkstone/ISEP#1
|
||
cb "前置是 s/triage(要不要做還沒決定)" "還沒分診" --title 好名字 "${OK[@]}" inkstone/ISEP#6
|
||
cb "前置已經是別的里程碑載體的前置(一張票同時只排一輪)" "只排一輪" --title 好名字 "${OK[@]}" inkstone/ISEP#7
|
||
cb "前置掛著子 repo 的里程碑" "已經掛在里程碑" --title 好名字 "${OK[@]}" inkstone/ISEP#8
|
||
p "★ 好幾格不對時一次列齊(Human 前置+沒分診前置,兩個理由都在)" "True" \
|
||
"rc==2 and not W and 'Arcrun#22:掛著 Human' in out and 'ISEP#1 還沒分診' in out" \
|
||
"$(j cmd_compose --title 好名字 "${OK[@]}" inkstone/Arcrun#22 inkstone/ISEP#1)"
|
||
p "參數層的錯(版本號名字)在打任何 API 之前就擋" "0" "len(rec)" \
|
||
"$(j cmd_compose --title 1.2.3 "${OK[@]}" inkstone/ISEP#5)"
|
||
|
||
echo
|
||
echo "── ④ 組了之後照里程碑往下做(驗收:再照里程碑往下做)──"
|
||
POOLA=$(j cmd_pick --mainline "$MAIN" --pool --all --json)
|
||
p "★ --pool:還沒到期的 InkStoneCo 里程碑「組好的 M」一組,成員是載體的前置(ISEP#7 的 s/backlog 不抓,Arcrun#21 s/todo 可抓)" \
|
||
"['inkstone/Arcrun#21']" "[r['ref'] for r in json.loads(out) if r['group']=='里程碑']" "$POOLA"
|
||
p "★ 載體票本身(InkStoneCo#80,s/backlog)不當成可做的票——里程碑那組與票池那組都沒有它" "False" \
|
||
"any(r['ref']=='inkstone/InkStoneCo#80' for r in json.loads(out))" "$POOLA"
|
||
p "里程碑那一組排在票池之前(先照里程碑做,再撿散票)" "True" \
|
||
"(lambda g: g.index('里程碑') < g.index('票池'))([r['group'] for r in json.loads(out)])" "$POOLA"
|
||
p "★ 待分診那一組最舊的先(ISEP#1 08-01 開),不含 Human/Leo" \
|
||
"['inkstone/ISEP#1', 'inkstone/ISEP#10', 'inkstone/ISEP#2', 'inkstone/InkStoneCo#55']" \
|
||
"[r['ref'] for r in json.loads(out) if r['group']=='待分診']" "$POOLA"
|
||
p "每一列都帶下一步(里程碑那一列是 claim、待分診那一列是 triage)" "True" \
|
||
"all((' triage ' in r['next']) == (r['group']=='待分診') for r in json.loads(out))" "$POOLA"
|
||
p "沒帶 --pool(本機總管)→ 不看還沒到期的里程碑、也不回待分診(誤攔比漏擋嚴重)" "True" \
|
||
"rc==1 and '里程碑「組好的' not in out and '待分診' not in out" "$(j cmd_pick --mainline "$MAIN")"
|
||
p "pick 只讀不寫" "True" "not W" "$POOLA"
|
||
|
||
echo
|
||
echo "── ⑤ 動詞接得上 ──"
|
||
out=$(python3 "$T" usage triage 2>&1); got=$?
|
||
if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--dup-of'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'ticket usage triage 印得出用法\n'
|
||
out=$(python3 "$T" usage compose 2>&1); got=$?
|
||
if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--carrier'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'ticket usage compose 印得出用法\n'
|
||
out=$(python3 "$T" triage inkstone/ISEP#1 --to s/backlog --why x 2>&1); got=$?
|
||
if [ "$got" != 0 ] && ! printf '%s' "$out" | grep -q '✅'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'triage 連不上 Gitea → 非零離開、不印成功(got=%s)\n' "$got"
|
||
|
||
echo
|
||
echo "通過 $PASS 條,失敗 $FAIL 條"
|
||
[ "$FAIL" = 0 ] || exit 1
|