#!/bin/bash # settings-allow-sync 的測試(inkstone/ISEP#130) # # 它守什麼: 「雲端 session 開工時就帶著跟本機同一份權限白名單」這件事的機器那一半。 # leo 09-07 親手加的四條只在本機 InkStoneCo/.claude/settings.json;雲端沒有 ⇒ 同樣動作被分類器擋 # (09-04 run log:permission_denials=6)。清單改住 ISEP 一份,本支把它寫進那台機器的 ~/.claude/settings.json。 # # 🔴 全程在 TMP 底下的假 settings.json 上跑,不碰真的 ~/.claude/settings.json。 cd "$(dirname "$0")/.." || exit 1 S=scripts/settings-allow-sync LIST=docs/permissions-allow.json TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0 ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; } bad(){ FAIL=$((FAIL+1)); printf ' ❌ %s\n' "$1"; } N=$(python3 -c "import json;print(len(json.load(open('$LIST'))['allow']))") echo "── A. 清單本身的形狀 ──" python3 - "$LIST" <<'PY' && ok "A1 每一條都是 Bash(…) 形狀,而且四個正門工具都在場(ticket/mainline/gate-ok/gitea-pr-merge)" || bad "A1 清單形狀不對或少了正門工具" import json, sys rules = json.load(open(sys.argv[1]))["allow"] assert rules and all(r.startswith("Bash(") and r.endswith(")") for r in rules), rules for tool in ("scripts/ticket", "scripts/mainline", "scripts/gate-ok", "scripts/gitea-pr-merge.sh"): assert any(tool in r for r in rules), "缺 " + tool assert not any(r.strip() in ("Bash(python3 *)", "Bash(*)", "Bash(bash *)") for r in rules), "整類放行" PY echo "── B. 寫進去(假的 settings.json)──" T="$TMP/a/settings.json" OUT=$(python3 "$S" --target "$T" --home /root 2>&1); RC=$? [ "$RC" = 0 ] && [ -f "$T" ] && ok "B1 目標不存在 ⇒ 建出來(離開碼 0)" || bad "B1 rc=$RC:$OUT" python3 -c "import json;d=json.load(open('$T'));assert len(d['permissions']['allow'])==$N" && ok "B2 寫進 $N 條,JSON 合法" || bad "B2 條數不對或不是 JSON" grep -q 'Bash(python3 /root/.claude/plugins/cache/inkstone/isep/\*/scripts/ticket \*)' "$T" \ && ok "B3 開頭的 ~ 換成了 --home 給的家目錄(雲端=/root)" || bad "B3 ~ 沒換成家目錄" grep -q '"~/' "$T" && bad "B4 檔裡還留著 ~" || ok "B4 檔裡沒有殘留的 ~" grep -q 'CLAUDE_PLUGIN_ROOT' "$T" && ok "B5 \$CLAUDE_PLUGIN_ROOT 那幾條原樣保留(閘印出來的逃生門就是這個形狀)" || bad "B5 少了 \$CLAUDE_PLUGIN_ROOT 形狀" tail -c1 "$T" | od -An -c | grep -q '\\n' && ok "B6 收尾有換行" || bad "B6 沒換行" SUM1=$(cksum < "$T"); OUT=$(python3 "$S" --target "$T" --home /root 2>&1); SUM2=$(cksum < "$T") [ "$SUM1" = "$SUM2" ] && printf '%s' "$OUT" | grep -q '已齊' && ok "B7 跑第二次:檔案一個位元組都沒變、說「已齊」(冪等)" || bad "B7 不冪等:$OUT" echo "── C. 只加不減:別人手加的東西一個字不動 ──" T2="$TMP/b/settings.json"; mkdir -p "$TMP/b" cat > "$T2" <<'JSON' { "extraKnownMarketplaces": {"inkstone": {"source": {"source": "git", "url": "https://git.uncle6.me/inkstone/ISEP.git"}}}, "enabledPlugins": {"isep@inkstone": true}, "permissions": { "allow": ["Bash(npx wrangler secret:*)", "Bash(python3 scripts/ticket *)"], "deny": ["Edit(//Users/x/wrangler.toml)"] }, "hooks": {"PreToolUse": []} } JSON OUT=$(python3 "$S" --target "$T2" --home /Users/youlinhsieh 2>&1) python3 - "$T2" "$N" <<'PY' && ok "C1 原有的 allow/deny/marketplace/hooks 全在,原本那條沒重複,只補了缺的" || bad "C1 動到了別人的東西:$(cat "$T2")" import json, sys d = json.load(open(sys.argv[1])); n = int(sys.argv[2]) a = d["permissions"]["allow"] assert a[0] == "Bash(npx wrangler secret:*)" and a[1] == "Bash(python3 scripts/ticket *)", a[:2] assert a.count("Bash(python3 scripts/ticket *)") == 1 assert len(a) == 2 + n - 1, (len(a), n) assert d["permissions"]["deny"] == ["Edit(//Users/x/wrangler.toml)"] assert d["enabledPlugins"] == {"isep@inkstone": True} assert d["hooks"] == {"PreToolUse": []} assert any("/Users/youlinhsieh/.claude/plugins/cache/inkstone/isep/*/scripts/mainline" in x for x in a) PY printf '%s' "$OUT" | grep -q "+$((N-1)) 條" && ok "C2 印出來的是「+$((N-1)) 條」(缺幾條補幾條)" || bad "C2 計數不對:$OUT" echo "── D. --check/--dry-run ──" python3 "$S" --target "$T" --home /root --check >/dev/null 2>&1 && ok "D1 --check:全齊 ⇒ 離開碼 0" || bad "D1 --check 全齊卻非 0" T3="$TMP/c/settings.json"; mkdir -p "$TMP/c"; echo '{}' > "$T3" python3 "$S" --target "$T3" --home /root --check >/dev/null 2>&1; RC=$? [ "$RC" = 1 ] && ok "D2 --check:缺 ⇒ 離開碼 1(驗收用)" || bad "D2 --check 缺卻回 $RC" [ "$(cat "$T3")" = "{}" ] && ok "D3 --check 不寫檔" || bad "D3 --check 寫了檔" python3 "$S" --target "$T3" --home /root --dry-run >/dev/null 2>&1 [ "$(cat "$T3")" = "{}" ] && ok "D4 --dry-run 不寫檔" || bad "D4 --dry-run 寫了檔" echo "── E. 壞掉的目標:不碰、不炸(這支掛在 SessionStart)──" T4="$TMP/d/settings.json"; mkdir -p "$TMP/d"; printf '{ not json' > "$T4" OUT=$(python3 "$S" --target "$T4" --home /root 2>&1); RC=$? [ "$RC" = 0 ] && [ "$(cat "$T4")" = "{ not json" ] && printf '%s' "$OUT" | grep -q '沒有動它' \ && ok "E1 目標不是 JSON ⇒ 原封不動、講出來、離開碼 0" || bad "E1 rc=$RC 內容=$(cat "$T4") out=$OUT" python3 "$S" --target "$T4" --home /root --check >/dev/null 2>&1; [ $? = 1 ] && ok "E2 同一個檔用 --check 問 ⇒ 離開碼 1(不裝沒事)" || bad "E2 --check 對壞檔回 0" echo "── F. 接線:SessionStart 有掛、雲端 setup script 有叫 ──" python3 - <<'PY' && ok "F1 hooks.json 的 SessionStart 註冊了 scripts/settings-allow-sync" || bad "F1 hooks.json 沒掛" import json d = json.load(open("hooks/hooks.json")) cmds = [h["command"] for g in d["hooks"]["SessionStart"] for h in g["hooks"]] assert any("scripts/settings-allow-sync" in c for c in cmds), cmds PY grep -q 'settings-allow-sync' docs/cloud-setup-script.sh && ok "F2 docs/cloud-setup-script.sh 裝完 plugin 會跑它" || bad "F2 setup script 沒叫它" grep -q 'exit 0$' docs/cloud-setup-script.sh && ok "F3 setup script 仍然 exit 0(非零會讓整個 session 開不起來)" || bad "F3 setup script 結尾不是 exit 0" echo; echo "$PASS/$((PASS+FAIL)) 通過" [ "$FAIL" -eq 0 ]