Files
ISEP/hooks/tests/mainline-members-by-dependency.test.sh
T
isep-hand 19face1c41 adopt 的冪等改靠「先讀 /dependencies」:真 Gitea 對重複相依回 500 不是 409(inkstone/ISEP#133)
真 Gitea 實跑(hub=InkStoneCo#44、子票=ISEP#133):第一次 POST 成功、/blocks 回 #44、
清空快取後 has 靠 /blocks 判「在」;第二次 POST 回 500——假伺服器原本猜 409,adopt 第二次會炸。
add_dependency() 改成先讀 parent 的 /dependencies,邊在就不 POST(判準是 Gitea 的事實,不是回應碼);
假伺服器改照真的回 500;A41 56→57 條。測完 DELETE 復原(201,/blocks 空)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
2026-09-07 10:46:43 +00:00

319 lines
25 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.
#!/usr/bin/env bash
# 主線成員「由相依邊決定,不靠同名里程碑」的迴歸測試(inkstone/ISEP#133 → comment 6743
#
# ── 這支在守什麼 ──────────────────────────────────────────────────────
# leo 2026-09-07:「拆,誰說『同名跨 repo 是 mainline 機制的一部分』,早就定了只有一個 milestone,
# 不同 repo 用指針」。上一版 `scripts/mainline` 的成員=「票所屬 milestone 的標題與主線相同(不分 repo)」,
# 總管照它做、還在 arcrun-rag 補建了一個同名的。這裡驗票上的三條驗法:
# A 成員=hub 里程碑裡的票 + hub 票 + 它們的相依(跨 repo);別 repo 同名里程碑裡的票**不算**
# B `adopt` 別 repo 的票 ⇒ 在 hub 票上加相依;**不會**在那個 repo 建同名里程碑;沒有 hub 票時
# 擋下並給兩條走得通的路(那條路真的跑一次)
# C 三個同名里程碑關掉之後,`mainline show``ticket pick``has`focus guard 仍然列得出跨 repo 成員
# D `refresh` 讀不到 Gitea ⇒ 成員保留(讀不到 ≠ 沒有成員)
# E `milestone-account` 的帳本落在 InkStoneCo/system-dev/,不落在 plugin 快取
#
# 用法:bash hooks/tests/mainline-members-by-dependency.test.sh
# 🔴 全程離線:Gitea 是本機一個假伺服器(TICKET_HOST 指過去),它照真 Gitea 回:相依 POST 201、
# 已存在 **500**2026-09-07 實測,不是 409)、issue 列表用 `milestones=` 名字查。狀態走 ISEP_COUNTDOWN_STATE_DIR、repo 那份走
# TMP 底下的假薄殼、HOME 也指到 TMP。不碰真家目錄、不碰真 Gitea、不碰真 InkStoneCo。
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MAINLINE="$ROOT/../scripts/mainline"
TICKET="$ROOT/../scripts/ticket"
ACCOUNT="$ROOT/../scripts/milestone-account"
GUARD="$ROOT/mainline-focus-guard.sh"
TMP=$(mktemp -d)
export HOME="$TMP/home"; mkdir -p "$HOME"
export ISEP_COUNTDOWN_STATE_DIR="$TMP/home-state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
export ISEP_COUNTDOWN_NOW=1757203200 # 2026-09-07 00:00 UTC
export ISEP_MAINLINE_GITEA_URL=none # 第三來源(ISEP#140)不在這支的範圍
export GITEA_TOKEN_CLAUDE_CODE=fake-token-for-test
unset ISEP_MAINLINE_REPO_FILE ISEP_MAINLINE_FIXTURE ISEP_MAINLINE_FILE CLAUDE_CODE_CHILD_SESSION
SHELL_ROOT="$TMP/shell"; HUB="$SHELL_ROOT/InkStoneCo"; mkdir -p "$HUB/system-dev" # 雲端薄殼的形狀
export CLAUDE_PROJECT_DIR="$SHELL_ROOT"
PASS=0; FAIL=0
ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
no(){ printf ' ❌ %s —— %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
# ── 假 Gitea:資料在 $TMP/world.json(測試中途可以改),每個請求記一行 `VERB path` 到 $TMP/log ──
cat > "$TMP/stub.py" <<'PY'
import json, os, re, sys, urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TMP = sys.argv[1]; PORT = int(sys.argv[2])
W = os.path.join(TMP, "world.json")
def world(): return json.load(open(W))
def save(w): json.dump(w, open(W, "w"), ensure_ascii=False)
def ref(r): return r
def issue_obj(w, r):
o, rest = r.split("/", 1); repo, n = rest.split("#")
d = w["issues"][r]
ms = d.get("milestone")
return {"number": int(n), "title": d.get("title", r), "state": d.get("state", "open"),
"labels": [{"name": l} for l in d.get("labels", [])],
"assignees": [{"login": a} for a in d.get("assignees", [])],
"milestone": (w["milestones"][ms] | {"id": int(ms.split("#")[1])}) if ms else None,
"created_at": "2026-09-01T00:00:00Z",
"repository": {"full_name": "%s/%s" % (o, repo)}}
def ms_obj(w, key):
o, rest = key.split("/", 1); repo, mid = rest.split("#")
return w["milestones"][key] | {"id": int(mid)}
class H(BaseHTTPRequestHandler):
def log_message(self, *a): pass
def _send(self, code, body):
raw = json.dumps(body, ensure_ascii=False).encode()
self.send_response(code); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
def _log(self):
with open(os.path.join(TMP, "log"), "a") as f: f.write("%s %s\n" % (self.command, self.path))
def do_GET(self):
self._log(); w = world()
if os.path.exists(os.path.join(TMP, "down")): return self._send(500, {"message": "boom"})
p, _, qs = self.path.partition("?"); q = urllib.parse.parse_qs(qs)
if p == "/api/v1/orgs/inkstone/repos": return self._send(200, [{"name": r} for r in w["repos"]])
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/milestones/(\d+)$", p)
if m:
key = "%s/%s#%s" % m.groups()
return self._send(200, ms_obj(w, key)) if key in w["milestones"] else self._send(404, {"message": "not found"})
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/milestones$", p)
if m:
pre = "%s/%s#" % m.groups(); st = q.get("state", ["open"])[0]
return self._send(200, [ms_obj(w, k) for k in w["milestones"] if k.startswith(pre)
and (st == "all" or w["milestones"][k]["state"] == st)])
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/issues$", p)
if m:
pre = "%s/%s#" % m.groups(); want = q.get("milestones", [""])[0]; st = q.get("state", ["open"])[0]
out = []
for r, d in w["issues"].items():
if not r.startswith(pre): continue
if st != "all" and d.get("state", "open") != st: continue
msk = d.get("milestone")
if want and (not msk or w["milestones"][msk]["title"] != want): continue
out.append(issue_obj(w, r))
return self._send(200, out)
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/issues/(\d+)(/dependencies|/blocks)?$", p)
if m:
r = "%s/%s#%s" % m.groups()[:3]
if r not in w["issues"]: return self._send(404, {"message": "not found"})
if not m.group(4): return self._send(200, issue_obj(w, r))
if m.group(4) == "/dependencies": return self._send(200, [issue_obj(w, c) for c in w["deps"].get(r, [])])
return self._send(200, [issue_obj(w, pr) for pr, cs in w["deps"].items() if r in cs])
return self._send(404, {"message": "not found", "path": p})
def do_POST(self):
self._log(); w = world()
n = int(self.headers.get("Content-Length") or 0); body = json.loads(self.rfile.read(n) or b"{}")
p = self.path.partition("?")[0]
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/issues/(\d+)/dependencies$", p)
if m:
parent = "%s/%s#%s" % m.groups(); child = "%s/%s#%s" % (body["owner"], body["repo"], body["index"])
if child in w["deps"].get(parent, []): return self._send(500, {"message": "", "url": "…/api/swagger"}) # 真 Gitea 2026-09-07 實測:重複相依回 500,不是 409
w["deps"].setdefault(parent, []).append(child); save(w)
return self._send(201, issue_obj(w, parent))
if re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/milestones$", p):
w.setdefault("created_milestones", []).append(p); save(w) # 🔴 這條不該被打到
return self._send(201, {"id": 999, "title": body.get("title")})
return self._send(404, {"message": "not found"})
def do_PATCH(self):
self._log(); w = world()
n = int(self.headers.get("Content-Length") or 0); body = json.loads(self.rfile.read(n) or b"{}")
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/issues/(\d+)$", self.path.partition("?")[0])
if m and "milestone" in body:
r = "%s/%s#%s" % m.groups(); key = "%s/%s#%s" % (m.group(1), m.group(2), body["milestone"])
w["issues"][r]["milestone"] = key; save(w)
return self._send(201, issue_obj(w, r))
return self._send(404, {"message": "not found"})
srv = ThreadingHTTPServer(("127.0.0.1", PORT), H); srv.daemon_threads = True; srv.serve_forever()
PY
# 世界:一條線「AI 問一次就看得到全部」,錨在 Arcrun#48;三個 repo 各有一個同名的(09-07 現場的形狀)
python3 - "$TMP/world.json" <<'PY'
import json, sys
T = "AI 問一次就看得到全部"
w = {
"repos": ["Arcrun", "mira", "ISEP", "arcrun-rag", "InkStoneCo"],
"milestones": {
"inkstone/Arcrun#48": {"title": T, "state": "open", "due_on": "2026-09-12T23:59:59Z", "open_issues": 3, "closed_issues": 0, "description": "兩個目錄讓 AI 查詢"},
"inkstone/Arcrun#50": {"title": "AI 問得到內文", "state": "open", "due_on": "2026-08-26T23:59:59Z", "open_issues": 1, "closed_issues": 1},
"inkstone/mira#49": {"title": T, "state": "open", "due_on": "2026-09-12T23:59:59Z", "open_issues": 1, "closed_issues": 0},
"inkstone/ISEP#60": {"title": T, "state": "open", "due_on": "2026-09-12T23:59:59Z", "open_issues": 1, "closed_issues": 1},
"inkstone/arcrun-rag#61": {"title": T, "state": "open", "due_on": "2026-09-12T23:59:59Z", "open_issues": 2, "closed_issues": 0},
},
"issues": {
"inkstone/Arcrun#86": {"milestone": "inkstone/Arcrun#48", "labels": ["p/high", "s/doing"]},
"inkstone/Arcrun#176": {"milestone": "inkstone/Arcrun#48", "labels": ["s/todo"]},
"inkstone/Arcrun#197": {"milestone": None, "labels": ["s/todo"]},
"inkstone/Arcrun#50": {"milestone": "inkstone/Arcrun#50", "labels": ["s/todo"]},
"inkstone/mira#6": {"milestone": "inkstone/mira#49", "labels": ["s/todo", "s/doing"]},
"inkstone/ISEP#130": {"milestone": "inkstone/ISEP#60", "labels": ["Human", "p/high", "s/todo"]},
"inkstone/ISEP#140": {"milestone": None, "labels": ["s/todo"]},
"inkstone/arcrun-rag#104": {"milestone": "inkstone/arcrun-rag#61", "labels": ["s/review"], "assignees": ["claude-code"]},
"inkstone/arcrun-rag#37": {"milestone": "inkstone/arcrun-rag#61", "labels": ["s/todo"]},
"inkstone/InkStoneCo#44": {"milestone": None, "labels": ["s/doing"]},
},
"deps": {
"inkstone/Arcrun#86": ["inkstone/mira#6"], # 09-07 現場:Arcrun#86 已經有跨 repo 相依
"inkstone/ISEP#130": ["inkstone/ISEP#140"], # 09-07 現場:#130 等 #140
"inkstone/InkStoneCo#44": ["inkstone/arcrun-rag#104"],# 09-07 現場:#44「這張票是那條線本身」
},
}
json.dump(w, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
PY
PORT=$(python3 -c "import socket;s=socket.socket();s.bind(('127.0.0.1',0));print(s.getsockname()[1])")
: > "$TMP/log"
python3 "$TMP/stub.py" "$TMP" "$PORT" & SRV=$!
trap 'kill $SRV 2>/dev/null; rm -rf "$TMP"' EXIT
for _ in $(seq 1 50); do python3 -c "import socket;socket.create_connection(('127.0.0.1',$PORT),1)" 2>/dev/null && break; sleep 0.1; done
export TICKET_HOST="http://127.0.0.1:$PORT"
ml(){ python3 "$MAINLINE" "$@" 2>&1; }
members(){ python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(' '.join(sorted(d.get('members') or [])))" "$ISEP_COUNTDOWN_STATE_DIR/mainline.json"; }
world_set(){ python3 - "$TMP/world.json" "$@" <<'PY'
import json, sys
w = json.load(open(sys.argv[1])); code = sys.argv[2]
exec(code)
json.dump(w, open(sys.argv[1], "w"), ensure_ascii=False)
PY
}
created_ms(){ python3 -c "import json,sys; print(len(json.load(open(sys.argv[1])).get('created_milestones') or []))" "$TMP/world.json"; }
dispatch(){ # 派工單 → "exit|stderr"
out=$(python3 -c 'import json,sys;print(json.dumps({"tool_input":{"prompt":sys.argv[1]},"session_id":"t"}))' "$1" \
| bash "$GUARD" 2>&1 >/dev/null); rc=$?
printf '%s|%s' "$rc" "$out"
}
echo "── A. set:成員=hub 里程碑裡的票 + hub 票 + 它們的相依;同名不算 ──"
OUT=$(ml set inkstone/Arcrun#48 --hub inkstone/InkStoneCo#44); RC=$?
[ "$RC" = 0 ] && ok "① set --hub 離開碼 0" || no "① set 失敗" "$OUT"
M=$(members)
if [[ " $M " == *" inkstone/Arcrun#86 "* && " $M " == *" inkstone/Arcrun#176 "* ]]; then ok "② hub 里程碑裡的兩張是成員(Arcrun#86、#176"; else no "② 里程碑裡的票沒收到" "$M"; fi
case "$M" in *"inkstone/mira#6"*) ok "③ mira#6 是成員——因為 Arcrun#86 把它當相依(跨 repo 邊)" ;; *) no "③ 相依沒走到 mira#6" "$M" ;; esac
case "$M" in *"inkstone/arcrun-rag#104"*) ok "④ arcrun-rag#104 是成員——因為 hub 票 InkStoneCo#44 把它當相依" ;; *) no "④ hub 票的相依沒收到" "$M" ;; esac
case "$M" in *"inkstone/InkStoneCo#44"*) ok "⑤ hub 票自己也是成員" ;; *) no "⑤ hub 票不在成員裡" "$M" ;; esac
case "$M" in *"inkstone/ISEP#130"*) no "⑥ ISEP#130 掛在 ISEP 的同名里程碑、沒有任何邊 ⇒ 不該是成員" "$M" ;; *) ok "⑥ ISEP#130 掛在 ISEP 裡**同名**的里程碑但沒有邊 ⇒ 不是成員(同名不算,leo 09-07" ;; esac
case "$M" in *"inkstone/arcrun-rag#37"*) no "⑦ arcrun-rag#37 同名沒邊 ⇒ 不該是成員" "$M" ;; *) ok "⑦ arcrun-rag#37arcrun-rag 同名里程碑裡、沒邊)也不是" ;; esac
case "$M" in *"inkstone/Arcrun#50"*) no "⑧ 別條線的票混進來了" "$M" ;; *) ok "⑧ 別條線(Arcrun#50)不在成員裡" ;; esac
grep -q '"hub_ticket": "inkstone/InkStoneCo#44"' "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" && ok "⑨ hub 票寫進 state(之後 adopt 不必再指定)" || no "⑨ hub_ticket 沒寫" "$(cat "$ISEP_COUNTDOWN_STATE_DIR/mainline.json")"
grep -q '"hub_ticket": "inkstone/InkStoneCo#44"' "$HUB/system-dev/mainline.json" && ok "⑩ repo 那份(InkStoneCo/system-dev)也一起寫了(ISEP#130 的鏡像沒被弄壞)" || no "⑩ 鏡像沒寫" "$(ls "$HUB/system-dev")"
grep -qE 'GET /api/v1/repos/inkstone/(mira|ISEP|arcrun-rag)/issues\?' "$TMP/log" && no "⑪ set 去別的 repo 列同名里程碑的票了" "$(grep issues? "$TMP/log")" || ok "⑪ set 只列 hub 里程碑那一個 repo 的票,不去別的 repo 找同名"
OUT=$(ml)
case "$OUT" in *"hub 票:inkstone/InkStoneCo#44"*) ok "⑫ show 講得出 hub 票是誰" ;; *) no "⑫ show 沒印 hub 票" "$OUT" ;; esac
case "$OUT" in *"Arcrun 2"*"InkStoneCo 1"*"arcrun-rag 1"*"mira 1"*) ok "⑬ show 的成員分佈跨四個 repo(沒有同名里程碑也列得出)" ;; *) no "⑬ show 分佈不對" "$OUT" ;; esac
echo "── B. adopt:別 repo ⇒ 加相依到 hub 票;不建同名里程碑;沒 hub 票時給走得通的路 ──"
: > "$TMP/log"
OUT=$(ml adopt inkstone/ISEP#130 --via inkstone/Arcrun#86); RC=$?
[ "$RC" = 0 ] && ok "⑭ adopt 別 repo 的票(--via 指到 hub 里程碑裡的一張)離開碼 0" || no "⑭ adopt 失敗" "$OUT"
grep -q '^POST /api/v1/repos/inkstone/Arcrun/issues/86/dependencies' "$TMP/log" && ok "⑮ 動作是 POST 相依到 Arcrun#86Gitea 原生邊)" || no "⑮ 沒有 POST 相依" "$(cat "$TMP/log")"
grep -q '^PATCH' "$TMP/log" && no "⑯ 別 repo 的票不該被改 milestone" "$(grep PATCH "$TMP/log")" || ok "⑯ 沒有去改那張票的 milestone(別 repo 的票不掛 hub 里程碑)"
[ "$(created_ms)" = 0 ] && ok "⑰ 沒有在 ISEP 建任何里程碑(同名里程碑不是出路)" || no "⑰ 建了里程碑" "$(created_ms)"
M=$(members)
case "$M" in *"inkstone/ISEP#130"*"inkstone/ISEP#140"*) ok "⑱ 成員多了 ISEP#130**還有它的相依 ISEP#140**(邊會一路走下去)" ;; *) no "⑱ 成員沒更新到 #130/#140" "$M" ;; esac
case "$OUT" in *"沒有多出任何里程碑"*) ok "⑲ 訊息明講那個 repo 沒有多出里程碑" ;; *) no "⑲ 訊息沒講" "$OUT" ;; esac
OUT=$(ml adopt inkstone/ISEP#130 --via inkstone/Arcrun#86); RC=$?
[ "$RC" = 0 ] && ok "⑳ 同一張再 adopt 一次 ⇒ 仍是離開碼 0(冪等:先讀 /dependencies,邊在就不 POST;真 Gitea 對重複 POST 回 500" || no "⑳ 重複 adopt 炸了" "$OUT"
grep -c '^POST /api/v1/repos/inkstone/Arcrun/issues/86/dependencies' "$TMP/log" | grep -qx 1 && ok "⑳′ 第二次沒有再 POST(判準是 Gitea 有沒有這條邊,不是回應碼)" || no "⑳′ 重複 POST 了" "$(grep POST "$TMP/log")"
: > "$TMP/log"
OUT=$(ml adopt inkstone/arcrun-rag#37); RC=$?
[ "$RC" = 0 ] && grep -q '^POST /api/v1/repos/inkstone/InkStoneCo/issues/44/dependencies' "$TMP/log" \
&& ok "㉑ 沒給 --via ⇒ 用 state 裡的 hub 票(InkStoneCo#44" || no "㉑ 沒用 hub 票" "$RC $OUT $(cat "$TMP/log")"
: > "$TMP/log"
OUT=$(ml adopt inkstone/Arcrun#197); RC=$?
[ "$RC" = 0 ] && grep -q '^PATCH /api/v1/repos/inkstone/Arcrun/issues/197' "$TMP/log" \
&& ok "㉒ 同 repo 的票 ⇒ 掛進 hub 里程碑(原生欄位),不加邊" || no "㉒ 同 repo 沒走 PATCH" "$RC $OUT $(cat "$TMP/log")"
grep -q '^POST' "$TMP/log" && no "㉓ 同 repo 不該 POST 相依" "$(grep POST "$TMP/log")" || ok "㉓ 同 repo 沒有 POST 相依"
# 沒有 hub 票的狀態:直接把 state 裡的 hub_ticket 拿掉
python3 - "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" "$HUB/system-dev/mainline.json" <<'PY'
import json, sys
for p in sys.argv[1:]:
d = json.load(open(p)); d.pop("hub_ticket", None); json.dump(d, open(p, "w"), ensure_ascii=False)
PY
world_set 'w["deps"]["inkstone/Arcrun#86"].remove("inkstone/ISEP#130")'
: > "$TMP/log"
OUT=$(ml adopt inkstone/ISEP#130); RC=$?
[ "$RC" = 2 ] && ok "㉔ 別 repo 的票、沒 hub 票、沒 --via ⇒ 擋(離開碼 2" || no "㉔ 沒擋" "$RC $OUT"
case "$OUT" in *"--hub"*"--via"*) ok "㉕ 訊息給兩條路:set --hubadopt --via" ;; *) no "㉕ 沒給路" "$OUT" ;; esac
case "$OUT" in *"不是**出路"*|*"不是出路"*) ok "㉖ 訊息明講「開同名里程碑不是出路」" ;; *) no "㉖ 沒講" "$OUT" ;; esac
case "$OUT" in *"inkstone/Arcrun#86"*) ok "㉗ 訊息列出 hub 里程碑裡有哪些票可以 --via(不用人自己去翻)" ;; *) no "㉗ 沒列候選" "$OUT" ;; esac
[ "$(created_ms)" = 0 ] && ok "㉘ 擋下時也沒建里程碑" || no "㉘ 建了" "$(created_ms)"
# 出路真的跑一次:把訊息裡 `adopt … --via <owner/repo#N>` 那行抽出來、填一格、執行
LINE=$(printf '%s\n' "$OUT" | grep -E '^\s+\S+ adopt inkstone/ISEP#130 --via ' | head -1 | sed 's/<owner\/repo#N>/inkstone\/Arcrun#86/')
OUT2=$(eval "$LINE" 2>&1); RC2=$?
[ "$RC2" = 0 ] && ok "㉙ 訊息裡那條出路原樣貼、只填一格 ⇒ 真的跑得通(離開碼 0)" || no "㉙ 出路跑不動" "LINE=$LINE$RC2 $OUT2"
echo "── C. 三個同名里程碑關掉之後(票上驗法第 1 條)──"
world_set '
for k in ("inkstone/mira#49","inkstone/ISEP#60","inkstone/arcrun-rag#61"): w["milestones"][k]["state"]="closed"
for r in ("inkstone/mira#6","inkstone/ISEP#130","inkstone/arcrun-rag#104","inkstone/arcrun-rag#37"): w["issues"][r]["milestone"]=None
'
python3 - "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1])); d["members"] = []; d["hub_ticket"] = "inkstone/InkStoneCo#44"; json.dump(d, open(sys.argv[1], "w"), ensure_ascii=False)
PY
rm -f "$HUB/system-dev/mainline.json"
: > "$TMP/log"
OUT=$(ml refresh); RC=$?
[ "$RC" = 0 ] && [ "$(members)" = "" ] && ! grep -q dependencies "$TMP/log" \
&& ok "㉚ 光 refreshSessionStart 那條)不走相依(成本不變:真 Gitea 25 張要 19 秒)" || no "㉚ refresh 走了相依或炸了" "$RC $OUT $(cat "$TMP/log")"
OUT=$(ml refresh --members); RC=$?
[ "$RC" = 0 ] && ok "㉚′ refresh --members 離開碼 0" || no "㉚′ refresh --members 失敗" "$OUT"
M=$(members)
for r in inkstone/mira#6 inkstone/ISEP#130 inkstone/ISEP#140 inkstone/arcrun-rag#104 inkstone/arcrun-rag#37; do
case " $M " in *" $r "*) ok "㉛ 同名里程碑關掉、票也不掛任何里程碑之後,$r 仍是成員(靠邊,不靠名字)" ;; *) no "㉛ $r 掉出主線了" "$M" ;; esac
done
OUT=$(ml)
case "$OUT" in *"mira 1"*"ISEP 2"*|*"ISEP 2"*"mira 1"*) ok "㉜ mainline show 列得出跨 repo 成員" ;; *) no "㉜ show 沒列出" "$OUT" ;; esac
OUT=$(python3 "$TICKET" pick --mainline "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" --all --json 2>&1); RC=$?
REFS=$(printf '%s' "$OUT" | python3 -c "import json,sys; print(' '.join(r['ref'] for r in json.load(sys.stdin) if r['group']=='主線'))" 2>/dev/null)
case " $REFS " in *" inkstone/ISEP#140 "*) ok "㉝ ticket pick 主線組抓得到 ISEP#140(沒里程碑、靠相依在線上、s/todo 沒人)" ;; *) no "㉝ pick 抓不到 ISEP#140" "$OUT" ;; esac
case " $REFS " in *" inkstone/arcrun-rag#37 "*) ok "㉞ ticket pick 也抓得到 arcrun-rag#37(同名里程碑關掉後靠 hub 票的邊)" ;; *) no "㉞ pick 抓不到 arcrun-rag#37" "$OUT" ;; esac
case " $REFS " in *"ISEP#130"*|*"mira#6"*|*"arcrun-rag#104"*) no "㉟ Humans/doing/有人的票被抓走了" "$REFS" ;; *) ok "㉟ ISEP#130Human)、mira#6s/doing)、arcrun-rag#104(有人)照舊不被抓" ;; esac
OUT=$(ml has inkstone/ISEP#140); RC=$?
[ "$RC" = 0 ] && ok "㊰ has ISEP#140 ⇒ 在(快取)" || no "has ISEP#140 說不在" "$OUT"
python3 - "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1])); d["members"] = ["inkstone/Arcrun#86", "inkstone/Arcrun#176"]; json.dump(d, open(sys.argv[1], "w"), ensure_ascii=False)
PY
OUT=$(ml has inkstone/mira#6); RC=$?
[ "$RC" = 0 ] && ok "㊱ 快取裡沒有 mira#6 ⇒ has 去問「誰把它當相依」(/blocks)⇒ 在" || no "㊱ has 靠 blocks 判不出來" "$RC $OUT"
OUT=$(ml has inkstone/Arcrun#50); RC=$?
[ "$RC" = 1 ] && ok "㊲ has Arcrun#50(別條線、沒人指著它)⇒ 不在(離開碼 1)" || no "㊲ has Arcrun#50 判錯" "$RC $OUT"
case "$OUT" in *"adopt inkstone/Arcrun#50"*) ok "㊳ 不在時給的出路是 adopt(不是去開同名里程碑)" ;; *) no "㊳ 出路不對" "$OUT" ;; esac
echo "── C. focus guard 的新鮮查詢:問 milestone /blocks,不問標題 ──"
: > "$TMP/log"
R=$(dispatch '【工單】inkstone/mira#6')
[ "${R%%|*}" = 0 ] && ok "㊴ 派快取裡沒有、但被 Arcrun#86 當相依的 mira#6 ⇒ 放行(靠 /blocks 學到)" || no "㊴ 誤攔 mira#6" "$R"
grep -q 'issues/6/blocks' "$TMP/log" && ok "㊵ 閘真的問了 /blocks" || no "㊵ 沒問 /blocks" "$(cat "$TMP/log")"
grep -q 'inkstone/mira#6' "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" && ok "㊶ 學到的成員寫回快取(下次不必再問)" || no "㊶ 沒寫回" "$(cat "$ISEP_COUNTDOWN_STATE_DIR/mainline.json")"
R=$(dispatch '【工單】inkstone/Arcrun#50')
[ "${R%%|*}" = 2 ] && ok "㊷ 派別條線的 Arcrun#50 ⇒ 擋一次" || no "㊷ 沒擋" "$R"
case "${R#*|}" in *"不要**去那個 repo 開同名里程碑"*|*"不要"*"同名里程碑"*) ok "㊸ 擋下時的補收出路明講「不要去開同名里程碑」" ;; *) no "㊸ 出路沒講" "${R#*|}" ;; esac
world_set 'w["milestones"]["inkstone/mira#49"]["state"]="open"; w["issues"]["inkstone/mira#5"]={"milestone":"inkstone/mira#49","labels":["s/todo"]}'
R=$(dispatch '【工單】inkstone/mira#5')
[ "${R%%|*}" = 2 ] && ok "㊹ 派一張掛在 mira **同名**里程碑、沒有任何邊的票 ⇒ 擋一次(同名不是主線)" || no "㊹ 同名被當成主線放行了" "$R"
R=$(dispatch '【工單】inkstone/mira#5')
[ "${R%%|*}" = 0 ] && ok "㊺ 重送 ⇒ 放行(至多擋一次)" || no "㊺ 鬼打牆" "$R"
echo "── D. refresh 讀不到 Gitea ⇒ 成員保留 ──"
BEFORE=$(members); touch "$TMP/down"
OUT=$(ml refresh --members); RC=$?
[ "$RC" = 0 ] && [ "$(members)" = "$BEFORE" ] && ok "㊻ Gitea 回 500 ⇒ refresh --members 不炸、成員一張不少(讀不到 ≠ 沒有成員)" || no "㊻ 成員被清掉或炸了" "$RC $OUT / $(members)"
rm -f "$TMP/down"
echo "── E. milestone-account 的帳本落在會被 clone 走的地方(票上驗法第 3 條)──"
OUT=$(CLAUDE_PROJECT_DIR="$SHELL_ROOT" python3 "$ACCOUNT" ledger-path 2>&1)
[ "$OUT" = "$HUB/system-dev/estimates/ledger.jsonl" ] && ok "㊼ 雲端薄殼形狀(\$CLAUDE_PROJECT_DIR 是薄殼根、真身在 InkStoneCo/)⇒ 帳本在 InkStoneCo/system-dev/estimates/" || no "㊼ 帳本路徑不對" "$OUT"
OUT=$(cd "$HUB" && env -u CLAUDE_PROJECT_DIR python3 "$ACCOUNT" ledger-path 2>&1)
[ "$OUT" = "$HUB/system-dev/estimates/ledger.jsonl" ] && ok "㊽ cwd 就是 InkStoneCo ⇒ 同一個地方" || no "㊽ cwd 形狀不對" "$OUT"
# plugin 快取的形狀:原始碼被複製到一個沒有 .git 的目錄(claude plugin update 會整個換掉)
CACHE="$TMP/plugins/cache/inkstone/isep/9.9.9"; mkdir -p "$CACHE/scripts" "$CACHE/hooks/lib" "$CACHE/system-dev"
cp "$ACCOUNT" "$CACHE/scripts/"; cp "$ROOT/lib/mainline.py" "$CACHE/hooks/lib/"
EMPTY="$TMP/shell-empty"; mkdir -p "$EMPTY"
OUT=$(cd "$EMPTY" && CLAUDE_PROJECT_DIR="$EMPTY" python3 "$CACHE/scripts/milestone-account" ledger-path 2>&1)
case "$OUT" in "$CACHE"*) no "㊾ 帳本落進 plugin 快取(下一版就不見)" "$OUT" ;; "$HOME/.claude/isep-estimates/ledger.jsonl") ok "㊾ 專案裡沒有 system-dev、原始碼又在沒 .git 的快取 ⇒ 帳本退到家目錄,**不落快取**" ;; *) no "㊾ 路徑不明" "$OUT" ;; esac
mkdir -p "$CACHE/.git"
OUT=$(cd "$EMPTY" && CLAUDE_PROJECT_DIR="$EMPTY" python3 "$CACHE/scripts/milestone-account" ledger-path 2>&1)
[ "$OUT" = "$CACHE/system-dev/estimates/ledger.jsonl" ] && ok "㊿ 同一棵樹有 .git(真的 checkoutdogfood 的形狀)⇒ 才准用它的 system-dev/" || no "㊿ checkout 形狀不對" "$OUT"
echo; echo "通過 $PASS 條,失敗 $FAIL 條"
[ "$FAIL" -eq 0 ]