65120a1c65
- docs/permissions-allow.json + scripts/settings-allow-sync:四個 Gitea 正門工具的權限白名單一份, setup script 裝完 plugin 寫一次、SessionStart 每次再對一次(只加不減、冪等) - hooks/lib/mainline.py/scripts/mainline:家目錄沒主線就讀 InkStoneCo/system-dev/mainline.json; set/adopt/clear 兩份一起寫,refresh 只寫家目錄 - hooks/leo21c-write-guard.sh:唯讀 -d(tr/cut/sort…)先剪掉再判、notify_leo trigger 放行 (與 prod-write-guard 同一份白名單)、改法段改印 09-02 起的 youlin 子網域;補第一支測試(26 條) - prod-write-guard/main-and-prod-push-guard/kbdb-live-exam:認得 youlin 新子網域 arcrun-yuga3bse - scripts/ticket:收件 repo 寫 inkstone/ISEP 不再 404(org 寫錯當場講) - scripts/isep-notify:有 TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID 先走 Bot API 直送(雲端唯一通的路) - 測試:A31–A35 共 79 條;README/plugin.json/hooks-inventory 數字實數(61 支、85 條、53 支腳本) 假設(記在這裡等 review):權限規則的形狀沿用 leo 09-07 親手加、實測有效的那四條; 「分類器真的不擋」要雲端一趟 run 的 permission_denials 才驗得到,本 PR 驗不了。 版本:待總管定版(plugin.json 仍 0.22.0)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTZ9QtvjY7MNxjfQbexAm7
96 lines
5.2 KiB
Bash
Executable File
96 lines
5.2 KiB
Bash
Executable File
#!/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/<repo>/…,沒有一條含 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
|