#!/usr/bin/env bash # 身份欄的兩道閘(inkstone/ISEP#30 comment 4325:「subagent 回覆時要表明身份」)。 # # 貼留言到票上有**兩條路**,所以測兩道閘: # 正門 scripts/ticket say / decide —— 檢查在打任何 API 之前,所以本測試離線跑得動 # 側門 直接打 Gitea API —— hooks/reply-identity-guard.sh # # **離線、不打網路。** 正門的案例全部在 `die()` 之前就結束,不會真的送出留言。 # # 用法:hooks/tests/reply-identity.test.sh set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$HERE/../.." && pwd)" HOOK="$ROOT/hooks/reply-identity-guard.sh" TICKET="$ROOT/scripts/ticket" TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0; N=0 ok() { printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); N=$((N+1)); } bad() { printf ' ❌ %s\n' "$1"; shift; printf '%s\n' "$*" | sed -n '1,6p' | sed 's/^/ /' FAIL=$((FAIL+1)); N=$((N+1)); } # ── 側門:hooks/reply-identity-guard.sh ───────────────────────────────── h() { # h <期望 exit> <說明> want="$1"; desc="$2"; cmd="$3" body=$(python3 - "$cmd" <<'PY' import json, sys print(json.dumps({"tool_name": "Bash", "tool_input": {"command": sys.argv[1]}}, ensure_ascii=False)) PY ) err=$(printf '%s' "$body" | bash "$HOOK" 2>&1 >/dev/null); rc=$? if [ "$rc" -eq "$want" ]; then ok "$desc"; else bad "$desc —— 期望 exit=$want,實得 exit=$rc" "$err"; fi } echo "── 側門(直接打 Gitea API)─────────────────────────────────────" h 2 "① POST 留言但沒有【身份】→ 擋" \ 'curl -X POST -H "Authorization: token $T" -d @body.json https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/30/comments' h 0 "② POST 留言且內文帶【身份】→ 放行" \ 'curl -X POST -d "{\"body\":\"【身份】subagent/inkstone/ISEP/feat/x\n做完了\"}" https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/30/comments' h 0 "③ 純讀取(GET 撈留言)→ 放行,這是最常做的動作,誤攔它比漏擋更糟" \ 'curl -s -H "Authorization: token $T" https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/30/comments?limit=60' h 0 "④ 走正門 scripts/ticket → 放行(正門有自己的閘,兩支同時擋會互相打架)" \ 'scripts/ticket say inkstone/ISEP#30 -F /tmp/body.md' h 0 "⑤ 開新票的端點(不帶票號)→ 不是本閘的地盤" \ 'curl -X POST -d @t.json https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues' h 0 "⑥ 逃生口 reply-identity-ok → 放行(留在指令歷史上)" \ 'curl -X POST -d @body.json https://git.uncle6.me/api/v1/repos/inkstone/ISEP/issues/30/comments # reply-identity-ok' h 0 "⑦ 只是在講這件事(把端點寫進文件)而沒有 POST → 不擋" \ 'echo "留言端點是 issues/30/comments" >> docs/notes.md' echo echo "── 正門(scripts/ticket)───────────────────────────────────────" g() { # g <期望 exit> <說明> <內文> want="$1"; desc="$2"; content="$3" printf '%s' "$content" > "$TMP/body.md" err=$(python3 "$TICKET" say inkstone/ISEP#30 -F "$TMP/body.md" 2>&1 >/dev/null); rc=$? if [ "$rc" -eq "$want" ]; then ok "$desc"; else bad "$desc —— 期望 exit=$want,實得 exit=$rc" "$err"; fi } g 2 "⑧ 內文沒有身份欄 → 在打 API 之前就擋(所以這一格離線也測得動)" \ '做完了,分支是 feat/x' g 2 "⑨ 有【身份】但角色不在三選一之內 → 擋" \ '【身份】機器人/inkstone/ISEP/feat/x 做完了' g 2 "⑩ 身份欄不在第一行 → 擋(要一眼看得到,不是藏在中間)" \ '做完了 【身份】subagent/inkstone/ISEP/feat/x' # ⑪ 合規的內文要能通過身份檢查——它會往下走到打 API, # 離線環境打不出去所以 exit 非 0;判準改成「錯誤訊息不是身份欄那一段」。 printf '%s' '【身份】subagent/inkstone/ISEP/feat/x 做完了' > "$TMP/ok.md" err=$(python3 "$TICKET" say inkstone/ISEP#30 -F "$TMP/ok.md" 2>&1 >/dev/null || true) if printf '%s' "$err" | grep -q '第一行要表明身份'; then bad "⑪ 合規的內文被身份閘擋掉了(誤攔)" "$err" else ok "⑪ 合規的內文通過身份閘(之後成不成功是網路的事,不是本閘的事)" fi echo echo "══ $PASS/$N 通過${FAIL:+,$FAIL 個失敗} ══" [ "$FAIL" -eq 0 ]