#!/usr/bin/env bash # gitea-arm-to-github-armed.sh 的迴歸測試——**保險寫在哪,讀的人就要在哪讀得到**。 # # 🔴 它在守什麼(inkstone/ISEP#156,2026-09-17 實撞,inkstone/arcrun-rag#202): # ISEP 以 plugin 形式裝在 `~/.claude/plugins/cache/inkstone/isep//`, # 舊版用 `ROOT=$(dirname $0)/..` 決定 `.github-armed` 寫在哪 # ⇒ 保險被寫進 **plugin 快取資料夾**;而出貨線(ship.mjs → d20-guard.mjs) # 與每一支讀它的閘(main-and-prod-push-guard/github-contact-guard/stage-before-prod-guard) # 讀的都是**專案根**(`${CLAUDE_PROJECT_DIR:-$(pwd)}`) # ⇒ leo 在票上回了碼、腳本印「✅ 保險已解」,出貨照樣卡住。 # **比單純失敗更糟:它對 leo 說謊。** # # 判準是「要求某個東西在場」:保險檔必須出現在讀的人會讀的那個位置, # 並且**真的被那些讀的人認得**(不是只驗檔案存在)。 # # 全程離線:`gitea-arm-check.sh` 的核准本身(Leo 真的回覆)必須打真的 Gitea, # 那部分由 gitea-arm-check.test.sh 與 inkstone/InkStoneCo#34 的人工實測負責。 # 這裡把「核准成功」換成 stub,**只驗核准之後保險落在哪、被不被認得**; # fail-closed 那條則用**真的** check(無待核請求 → 本地就擋,不碰網路)。 # # 用法:bash hooks/tests/gitea-arm-to-github-armed.test.sh . set -u REPO="$(cd "${1:?用法: $0 }" && pwd)" SRC="$REPO/scripts/gitea-arm-to-github-armed.sh" [ -f "$SRC" ] || { echo "❌ 找不到 $SRC(參數要傳 ISEP repo 根目錄)"; exit 1; } T=$(mktemp -d) trap '[ -n "${KEEP:-}" ] && echo "保留 $T" || rm -rf "$T"' EXIT # ── 造一個長得像 plugin 快取的安裝位置,與一個長得像 InkStoneCo 的專案根 ── PLUG="$T/.claude/plugins/cache/inkstone/isep/9.9.9" PROJ="$T/InkStoneCo" mkdir -p "$PLUG/scripts/lib" "$PROJ" "$T/elsewhere" cp "$SRC" "$PLUG/scripts/" cp "$REPO/scripts/lib/gitea-arm-common.sh" "$PLUG/scripts/lib/" cp "$REPO/scripts/gitea-arm-request.sh" "$PLUG/scripts/" 2>/dev/null # stub:扮演「leo 已在票上核准」——輸出格式照 gitea-arm-check.sh 成功時的 `ARMED: ` cat > "$PLUG/scripts/gitea-arm-check.sh" <<'STUB' #!/bin/sh [ "${1:-}" = "--consume" ] || { echo "stub: 應該帶 --consume" >&2; exit 1; } echo "ARMED: 測試出貨 ISEP#156" STUB chmod +x "$PLUG/scripts/"*.sh PASS=0; FAIL=0 ok(){ echo " ✅ $1"; PASS=$((PASS+1)); } ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); } chk(){ if eval "$2"; then ok "$1"; else ng "$1"; fi; } echo "── ① 從 plugin 路徑執行,CLAUDE_PROJECT_DIR 指向專案根(總管 session 的實際形狀)──" ( cd "$T/elsewhere" && CLAUDE_PROJECT_DIR="$PROJ" sh "$PLUG/scripts/gitea-arm-to-github-armed.sh" ) >"$T/out1" 2>&1 rc=$? chk "離開碼 0" '[ $rc -eq 0 ]' chk "★ 專案根出現 .github-armed" '[ -f "$PROJ/.github-armed" ]' chk "★ plugin 快取資料夾裡沒有 .github-armed(09-17 實撞的位置)" '[ ! -e "$PLUG/.github-armed" ]' chk "cwd 也沒有被寫(不是靠碰巧站對位置)" '[ ! -e "$T/elsewhere/.github-armed" ]' chk "印出的「提前上保險」路徑就是真正寫下的那份" 'grep -qF "rm $PROJ/.github-armed" "$T/out1"' chk "第 1 行是未來的到期 epoch" '[ "$(sed -n 1p "$PROJ/.github-armed")" -gt "$(date +%s)" ]' chk "第 2 行帶任務說明" '[ "$(sed -n 2p "$PROJ/.github-armed")" = "測試出貨 ISEP#156" ]' echo "── ② 讀的人真的認得這份保險(不是只驗檔案存在)──" # ②a 出貨線:ship.mjs 的 prod --confirm preflight 呼叫的就是 d20-guard.mjs 的 checkArmed(INKSTONE_ROOT) D20="" for c in "${INKSTONE_ROOT:-}" "$REPO/../InkStoneCo" "$REPO/../../InkStoneCo" "$HOME/Documents/tech_projects/InkStoneCo"; do [ -n "$c" ] && [ -f "$c/products/arcrun-rag/installer/scripts/d20-guard.mjs" ] \ && { D20="$(cd "$c" && pwd)/products/arcrun-rag/installer/scripts/d20-guard.mjs"; break; } done if [ -n "$D20" ] && command -v node >/dev/null 2>&1; then node --input-type=module -e " const { checkArmed } = await import(process.argv[1]); const r = checkArmed(process.argv[2]); console.log('checkArmed OK mission=' + r.mission); " "$D20" "$PROJ" >"$T/out2" 2>&1 rc=$? chk "★ ship.mjs 的 checkArmed(專案根) 認得($(head -1 "$T/out2"))" '[ $rc -eq 0 ]' node --input-type=module -e " const { checkArmed } = await import(process.argv[1]); checkArmed(process.argv[2]); " "$D20" "$PLUG" >/dev/null 2>&1 rc=$? chk "對照:checkArmed(plugin 快取) 讀不到 ⇒ 上一條有鑑別力" '[ $rc -ne 0 ]' else echo " ⏭️ SKIP 找不到 arcrun-rag 的 d20-guard.mjs 或 node(不算失敗,也不算驗過)" fi # ②b 閘:同一個位置(`${CLAUDE_PROJECT_DIR:-$(pwd)}/.github-armed`) mk(){ python3 -c "import json,sys;print(json.dumps({'tool_name':'Bash','tool_input':{'command':sys.argv[1]},'cwd':sys.argv[2]}))" "$1" "$T/elsewhere"; } gate(){ # gate → 回 hook 離開碼 mk "$2" | ( cd "$T/elsewhere" && env -u CLAUDE_CODE_CHILD_SESSION CLAUDE_PROJECT_DIR="$3" bash "$1" ) >/dev/null 2>&1 } # 閘會寫 github-contact-log/狀態目錄——一律寫進暫存的專案根,不碰真的 InkStoneCo mkdir -p "$PROJ/system-dev/docs/3-specs/autonomy-dispatch" "$T/empty/system-dev/docs/3-specs/autonomy-dispatch" WR="npx wrangler deploy" GH="git push https://github.com/example/example.git main" gate "$REPO/hooks/main-and-prod-push-guard.sh" "$WR" "$PROJ"; rc=$? chk "★ main-and-prod-push-guard:保險在專案根 ⇒ wrangler deploy 放行" '[ $rc -ne 2 ]' gate "$REPO/hooks/main-and-prod-push-guard.sh" "$WR" "$T/empty"; rc=$? chk "對照:沒有保險 ⇒ 同一條被擋" '[ $rc -eq 2 ]' gate "$REPO/hooks/github-contact-guard.sh" "$GH" "$PROJ"; rc=$? chk "★ github-contact-guard:保險在專案根 ⇒ push github 放行" '[ $rc -ne 2 ]' gate "$REPO/hooks/github-contact-guard.sh" "$GH" "$T/empty"; rc=$? chk "對照:沒有保險 ⇒ 同一條被擋" '[ $rc -eq 2 ]' echo "── ③ 沒設 CLAUDE_PROJECT_DIR(leo 在終端機)⇒ 跟閘一樣退回 cwd ──" rm -f "$PROJ/.github-armed" ( cd "$PROJ" && env -u CLAUDE_PROJECT_DIR sh "$PLUG/scripts/gitea-arm-to-github-armed.sh" ) >"$T/out3" 2>&1 rc=$? chk "離開碼 0" '[ $rc -eq 0 ]' chk "寫在 cwd(專案根),不在 plugin 快取" '[ -f "$PROJ/.github-armed" ] && [ ! -e "$PLUG/.github-armed" ]' echo "── ④ fail-closed:沒拿到核准就一個檔都不寫(用真的 check,不碰網路)──" rm -f "$PROJ/.github-armed" "$PLUG/.github-armed" cp "$REPO/scripts/gitea-arm-check.sh" "$PLUG/scripts/gitea-arm-check.sh" printf 'GITEA_TOKEN_CLAUDE_CODE=test-token-not-real\n' > "$PROJ/.env" ( cd "$T/elsewhere" && CLAUDE_PROJECT_DIR="$PROJ" sh "$PLUG/scripts/gitea-arm-to-github-armed.sh" ) >"$T/out4" 2>&1 rc=$? chk "沒有待核請求 ⇒ 離開碼非 0" '[ $rc -ne 0 ]' chk "專案根與 plugin 快取都沒有 .github-armed" '[ ! -e "$PROJ/.github-armed" ] && [ ! -e "$PLUG/.github-armed" ]' # 出路要走得通:訊息印的那支 request 腳本必須是真的存在的絕對路徑 # (相對的 `scripts/…` 會對貼上那個人的 cwd 解析,站在 InkStoneCo 會叫到舊複本——ISEP#112 同一個病) # 取「本檔自己印的」那一行(⛔ 之後);中文冒號不是空白,所以用「⛔ 之後第一個以 / 開頭的 token」 REQ=$(sed -n '/⛔/,$p' "$T/out4" | grep -oE '[^[:space:]:]*gitea-arm-request\.sh' | grep '^/' | head -1) chk "★ 出路印的是存在的絕對路徑(印出:${REQ:-無})" '[ -n "$REQ" ] && [ -f "$REQ" ]' echo echo "通過 $PASS 條,失敗 $FAIL 條" [ "$FAIL" -eq 0 ]