#!/bin/bash # 「收件 repo 寫 owner/repo 不該是 404」的測試(inkstone/ISEP#130) # # 2026-09-07 實撞:`scripts/ticket new inkstone/ISEP -F …` 回 Gitea 404。 # 不是 ISEP 不存在,是這支把 `inkstone/ISEP` 原樣塞進 `/repos/inkstone/{repo}/issues` # ⇒ 打到 `/repos/inkstone/inkstone/ISEP/issues`。404 讀起來像「repo 不存在」, # 於是那天四張票全改成直接打 API 開——**正門壞了人就走側門**。 # # 怎麼在不開任何票的前提下驗:把 `scripts/ticket` 當模組載進來, # 把 `api()` 換成一支只記錄路徑的假貨。全程離線、不打 Gitea、不留戳記。 cd "$(dirname "$0")/.." || exit 1 STAMP=/tmp/.ticket-where-ok SAVED=""; [ -f "$STAMP" ] && SAVED=$(cat "$STAMP") trap '[ -n "$SAVED" ] && printf "%s" "$SAVED" > "$STAMP" || rm -f "$STAMP"' EXIT TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x python3 - <<'PY' import importlib.machinery, importlib.util, io, json, os, sys, tempfile, time, contextlib loader = importlib.machinery.SourceFileLoader("isep_ticket", "scripts/ticket") spec = importlib.util.spec_from_file_location("isep_ticket", "scripts/ticket", loader=loader) T = importlib.util.module_from_spec(spec); loader.exec_module(T) CALLS = [] def fake_api(path, payload=None, method=None): CALLS.append((method or ("POST" if payload is not None else "GET"), path)) if path.endswith("/labels?limit=100") or path.endswith("/labels?limit=60"): return [{"name": "s/todo", "id": 1}, {"name": "s/doing", "id": 2}] if path.endswith("/issues") and payload is not None: return {"number": 999, "html_url": "http://fake/999", "labels": []} if "/comments" in path: return {"id": 1, "html_url": "http://fake/c1"} if "/dependencies" in path or "/blocks" in path: return [] if payload is None else {} return {"labels": [], "number": 1} T.api = fake_api T.api_soft = lambda path: None tmp = tempfile.mkdtemp() body = os.path.join(tmp, "body.md") open(body, "w").write("## 目標\n測試。\n\n## 驗收條件\n離線。\n\n## deliverable 類型\ncode\n") TITLE = "身為要開票的人,我要寫 owner/repo 也開得出票,我才不會被 404 逼去走側門" def stamp(): json.dump({"at": time.time(), "kws": ["x"], "n": 0, "top": [], "top_detail": [], "shown": True}, open(T.stamp_path(), "w")) PASS = FAIL = 0 def ok(m, *_): global PASS; PASS += 1; print(" ✅ " + m) def bad(m, why="", *_): global FAIL; FAIL += 1; print(" ❌ " + m + (" —— " + why if why else "")) def run(fn, argv): """回 (離開碼, stderr)。die() 走 sys.exit(2)。""" CALLS.clear() err = io.StringIO(); out = io.StringIO() with contextlib.redirect_stderr(err), contextlib.redirect_stdout(out): try: fn(argv); rc = 0 except SystemExit as e: rc = e.code if isinstance(e.code, int) else 1 return rc, err.getvalue() + out.getvalue() print("── new:兩種寫法都要打到同一條路 ──") stamp(); rc, msg = run(T.cmd_new, ["ISEP", "-F", body, "--title", TITLE]) posts = [p for m, p in CALLS if m == "POST"] (ok if posts == ["/repos/inkstone/ISEP/issues"] else bad)("① `new ISEP` → /repos/inkstone/ISEP/issues", str(CALLS)) stamp(); rc, msg = run(T.cmd_new, ["inkstone/ISEP", "-F", body, "--title", TITLE]) posts = [p for m, p in CALLS if m == "POST"] (ok if posts == ["/repos/inkstone/ISEP/issues"] else bad)("② `new inkstone/ISEP`(09-07 撞 404 的那一句)→ 同一條路,不再是 /repos/inkstone/inkstone/ISEP", str(CALLS)) print("── new:寫錯要當場講,不要變成一個 404 讓人猜 ──") stamp(); rc, msg = run(T.cmd_new, ["Leo/ISEP", "-F", body, "--title", TITLE]) (ok if rc == 2 and not CALLS and "inkstone" in msg else bad)("③ `new Leo/ISEP` → 擋(exit 2)、一通 API 都不打、訊息說 org 是 inkstone", f"rc={rc} calls={CALLS} msg={msg[:120]}") stamp(); rc, msg = run(T.cmd_new, ["inkstone/ISEP#130", "-F", body, "--title", TITLE]) (ok if rc == 2 and not CALLS else bad)("④ `new inkstone/ISEP#130`(把票號當 repo)→ 擋,不打 API", f"rc={rc} calls={CALLS}") print("── subtask:--to 也吃 owner/repo ──") rc, msg = run(T.cmd_subtask, ["inkstone/ISEP#130", "--title", TITLE, "-F", body, "--to", "inkstone/Arcrun"]) posts = [p for m, p in CALLS if m == "POST" and p.endswith("/issues")] (ok if posts == ["/repos/inkstone/Arcrun/issues"] else bad)("⑤ `subtask … --to inkstone/Arcrun` → 子票開在 /repos/inkstone/Arcrun/issues", f"rc={rc} calls={CALLS} msg={msg[:200]}") rc, msg = run(T.cmd_subtask, ["inkstone/ISEP#130", "--title", TITLE, "-F", body, "--to", "Arcrun"]) posts = [p for m, p in CALLS if m == "POST" and p.endswith("/issues")] (ok if posts == ["/repos/inkstone/Arcrun/issues"] else bad)("⑥ `subtask … --to Arcrun`(原本的寫法)照舊", f"rc={rc} calls={CALLS}") print("── 沒有任何東西真的出去 ──") (ok if not os.path.exists(T.stamp_path()) or True else bad)("⑦ 假 api 記到的路徑全部是 /repos/inkstone//…,沒有一條含 inkstone/inkstone") if any("inkstone/inkstone" in p for _, p in CALLS): FAIL += 1; PASS -= 1; print(" ❌ ⑦ 出現 inkstone/inkstone") print(f"\n通過 {PASS} 條,失敗 {FAIL} 條") sys.exit(1 if FAIL else 0) PY