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
118 lines
7.7 KiB
Bash
Executable File
118 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 主線那個檔「隨 repo 走」的迴歸測試(inkstone/ISEP#130)
|
||
#
|
||
# ── 這支在守什麼 ──────────────────────────────────────────────────────
|
||
# 主線原本只住在家目錄(`~/.claude/isep-countdown/mainline.json`)。雲端是另一台機器,
|
||
# 家目錄空的、clone 下來的 repo 才是它拿得到的東西 ⇒ 2026-09-05 leo:
|
||
# 「routine 每天早上開啟後抓不到任務」。PR inkstone/InkStoneCo#118 把主線檔放進
|
||
# `InkStoneCo/system-dev/mainline.json`,這裡驗 ISEP 這一半:
|
||
# A 讀:家目錄沒有 ⇒ 讀 repo 那份(雲端的形狀:$CLAUDE_PROJECT_DIR 是薄殼根,真身在 InkStoneCo/)
|
||
# B 兩份都在 ⇒ `set_at` 較新的算數,不分本機雲端
|
||
# C 寫:`set`/`adopt`/`clear` 兩份一起動;`refresh` 那種只寫家目錄(不弄髒 InkStoneCo 工作樹)
|
||
# D 哪裡都沒有 ⇒ 仍然答得出「現在沒有主線」(ISEP#82 驗收第 4 條沒有被本票弄壞)
|
||
#
|
||
# 用法:bash hooks/tests/mainline-repo-mirror.test.sh
|
||
# 🔴 全程離線:狀態走 ISEP_COUNTDOWN_STATE_DIR、repo 那份走 TMP 底下的假 InkStoneCo、
|
||
# TICKET_HOST 指到連不上的位址。不碰真家目錄、不碰真 InkStoneCo。
|
||
set -u
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
MAINLINE="$ROOT/../scripts/mainline"
|
||
LIB="$ROOT/lib/mainline.py"
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||
export ISEP_COUNTDOWN_STATE_DIR="$TMP/home-state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
|
||
export TICKET_HOST="http://127.0.0.1:9"
|
||
unset ISEP_MAINLINE_REPO_FILE ISEP_MAINLINE_FIXTURE
|
||
SHELL_ROOT="$TMP/shell"; HUB="$SHELL_ROOT/InkStoneCo"; mkdir -p "$HUB/system-dev"
|
||
PASS=0; FAIL=0
|
||
ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
|
||
no(){ printf ' ❌ %s —— %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
|
||
|
||
write_ms(){ # write_ms <檔> <標題> <set_at>
|
||
python3 - "$1" "$2" "$3" <<'PY'
|
||
import json, sys
|
||
json.dump({"ref": "inkstone/Arcrun#48", "owner": "inkstone", "repo": "Arcrun", "id": 48,
|
||
"title": sys.argv[2], "description": "測試用。", "due_on": "2026-09-13T23:59:59Z",
|
||
"open_issues": 3, "closed_issues": 0, "members": ["inkstone/ISEP#130"],
|
||
"set_at": sys.argv[3], "refreshed_at": sys.argv[3]},
|
||
open(sys.argv[1], "w"), ensure_ascii=False, indent=2)
|
||
PY
|
||
}
|
||
show(){ CLAUDE_PROJECT_DIR="$1" python3 "$MAINLINE" 2>&1 | head -1; }
|
||
|
||
echo "── A. 家目錄沒有 ⇒ 讀 repo 那份 ──"
|
||
write_ms "$HUB/system-dev/mainline.json" "雲端才讀得到的那一條" "2026-09-07 07:30"
|
||
OUT=$(show "$SHELL_ROOT")
|
||
case "$OUT" in *"雲端才讀得到的那一條"*) ok "① \$CLAUDE_PROJECT_DIR 是薄殼根、真身在 InkStoneCo/(雲端的形狀)⇒ 答得出主線" ;;
|
||
*) no "① 薄殼根形狀讀不到 repo 那份" "$OUT" ;; esac
|
||
OUT=$(show "$HUB")
|
||
case "$OUT" in *"雲端才讀得到的那一條"*) ok "② \$CLAUDE_PROJECT_DIR 直接是 InkStoneCo ⇒ 也答得出" ;;
|
||
*) no "② InkStoneCo 根讀不到" "$OUT" ;; esac
|
||
OUT=$(cd "$HUB" && env -u CLAUDE_PROJECT_DIR python3 "$MAINLINE" 2>&1 | head -1)
|
||
case "$OUT" in *"雲端才讀得到的那一條"*) ok "③ 沒設 \$CLAUDE_PROJECT_DIR、只是 cwd 在 InkStoneCo ⇒ 也答得出" ;;
|
||
*) no "③ cwd 形狀讀不到" "$OUT" ;; esac
|
||
[ ! -e "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" ] && ok "④ 只是讀,家目錄那份沒有被順手建出來" \
|
||
|| no "④ 讀的時候寫了家目錄" "$(ls "$ISEP_COUNTDOWN_STATE_DIR")"
|
||
|
||
echo "── B. 兩份都在 ⇒ set_at 較新的算數 ──"
|
||
write_ms "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" "本機舊的" "2026-09-01 10:00"
|
||
OUT=$(show "$SHELL_ROOT")
|
||
case "$OUT" in *"雲端才讀得到的那一條"*) ok "⑤ repo 那份較新(別台機器後標的)⇒ 用 repo 那份" ;;
|
||
*) no "⑤ 沒用較新的 repo 那份" "$OUT" ;; esac
|
||
write_ms "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" "本機新的" "2026-09-08 09:00"
|
||
OUT=$(show "$SHELL_ROOT")
|
||
case "$OUT" in *"本機新的"*) ok "⑥ 家目錄那份較新 ⇒ 用家目錄那份" ;;
|
||
*) no "⑥ 沒用較新的家目錄那份" "$OUT" ;; esac
|
||
write_ms "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" "同時標的(家目錄)" "2026-09-07 07:30"
|
||
OUT=$(show "$SHELL_ROOT")
|
||
case "$OUT" in *"同時標的(家目錄)"*) ok "⑦ 一樣新 ⇒ 家目錄那份(它才有 refresh 過的進度)" ;;
|
||
*) no "⑦ 平手沒選家目錄" "$OUT" ;; esac
|
||
|
||
echo "── C. 寫:誰該動 repo 那份 ──"
|
||
R=$(CLAUDE_PROJECT_DIR="$SHELL_ROOT" python3 - "$LIB" "$HUB/system-dev/mainline.json" <<'PY'
|
||
import importlib.util, json, os, sys
|
||
spec = importlib.util.spec_from_file_location("ml", sys.argv[1]); ML = importlib.util.module_from_spec(spec); spec.loader.exec_module(ML)
|
||
rp = sys.argv[2]
|
||
before = open(rp).read()
|
||
d = ML.load(); d["refreshed_at"] = "2026-09-09 00:00"; d["open_issues"] = 1
|
||
ML.save(d) # refresh 的形狀:只寫家目錄
|
||
r1 = open(rp).read() == before
|
||
d["title"] = "set 之後的新主線"; d["set_at"] = "2026-09-09 00:01"
|
||
ML.save(d, mirror=True) # set/adopt 的形狀:兩份一起
|
||
r2 = json.load(open(rp)).get("title") == "set 之後的新主線"
|
||
r3 = json.load(open(ML.path())).get("title") == "set 之後的新主線"
|
||
r4 = open(rp).read().endswith("\n")
|
||
print("|".join(str(x) for x in (r1, r2, r3, r4)))
|
||
PY
|
||
)
|
||
[ "${R%%|*}" = "True" ] && ok "⑧ save()(refresh 的形狀)不碰 repo 那份——不弄髒 InkStoneCo 工作樹" || no "⑧ refresh 動了 repo 那份" "$R"
|
||
R2=${R#*|}; [ "${R2%%|*}" = "True" ] && ok "⑨ save(mirror=True)(set/adopt 的形狀)寫進 repo 那份" || no "⑨ mirror 沒寫 repo" "$R"
|
||
R3=${R2#*|}; [ "${R3%%|*}" = "True" ] && ok "⑩ 同一次也寫了家目錄那份" || no "⑩ 家目錄沒寫" "$R"
|
||
[ "${R##*|}" = "True" ] && ok "⑪ repo 那份收尾有換行(diff 才不會每次多一行 no newline)" || no "⑪ 沒換行" "$R"
|
||
|
||
OUT=$(CLAUDE_PROJECT_DIR="$SHELL_ROOT" python3 "$MAINLINE" clear 2>&1)
|
||
if [ ! -e "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" ] && [ ! -e "$HUB/system-dev/mainline.json" ]; then
|
||
ok "⑫ clear ⇒ 兩份都拿掉(否則下次讀又從 repo 那份長回來)"
|
||
else no "⑫ clear 沒清乾淨" "$(ls "$ISEP_COUNTDOWN_STATE_DIR" "$HUB/system-dev" 2>&1 | tr '\n' ' ')"; fi
|
||
case "$OUT" in *"repo 那份也刪了"*) ok "⑬ clear 講得出 repo 那份也動了(要 commit 才到雲端)" ;; *) no "⑬ clear 沒講 repo 那份" "$OUT" ;; esac
|
||
|
||
# set 要打網路,這裡只驗它印出來的那段「repo 那份寫了沒」是接在 repo_path 上的
|
||
R=$(CLAUDE_PROJECT_DIR="$SHELL_ROOT" python3 - "$LIB" "$HUB" <<'PY'
|
||
import importlib.util, os, sys
|
||
spec = importlib.util.spec_from_file_location("ml", sys.argv[1]); ML = importlib.util.module_from_spec(spec); spec.loader.exec_module(ML)
|
||
print(ML.repo_path(for_write=True) == os.path.join(sys.argv[2], "system-dev", "mainline.json"))
|
||
PY
|
||
)
|
||
[ "$R" = "True" ] && ok "⑭ 檔還不存在時,寫的目標仍指到 InkStoneCo/system-dev/(set 建得出它)" || no "⑭ 寫的目標不對" "$R"
|
||
|
||
echo "── D. 哪裡都沒有 ⇒ 仍然答得出「現在沒有主線」 ──"
|
||
OUT=$(show "$SHELL_ROOT")
|
||
case "$OUT" in *"現在沒有主線"*) ok "⑮ 兩份都沒有 ⇒ 「現在沒有主線」(ISEP#82 第 4 條沒被弄壞)" ;; *) no "⑮ 沒主線時答不出來" "$OUT" ;; esac
|
||
OUT=$(ISEP_MAINLINE_REPO_FILE=none show "$SHELL_ROOT")
|
||
case "$OUT" in *"現在沒有主線"*) ok "⑯ ISEP_MAINLINE_REPO_FILE=none ⇒ 不看 repo(既有測試靠它把外界定住)" ;; *) no "⑯ none 沒生效" "$OUT" ;; esac
|
||
printf 'not json' > "$HUB/system-dev/mainline.json"
|
||
OUT=$(show "$SHELL_ROOT")
|
||
case "$OUT" in *"現在沒有主線"*) ok "⑰ repo 那份壞掉 ⇒ 當成沒有,不炸" ;; *) no "⑰ 壞檔炸了" "$OUT" ;; esac
|
||
|
||
echo; echo "通過 $PASS 條,失敗 $FAIL 條"
|
||
[ "$FAIL" -eq 0 ]
|