Files
ISEP/scripts/test-unreleased-police.sh
T
Leo 0024d697f9 feat(hooks): 未出貨警察 unreleased-police.sh —— 收工那刻抓「改了 repo 卻沒出貨」(inkstone/ISEP#68)
leo 2026-08-27:「每個 repo 只要有改動就要出貨⋯⋯這個 session 有改但要
結束沒出貨就抓,你可以久一點再出貨累積幾個 PR,但最後一定要出貨」。

四個判準(票要求自己判、寫得出理由,不照抄總管):
- 「改過」=這個 session 期間 HEAD 新長出來的 commit(不是工作區髒、不是 push)。
  與「出貨」用同一個單位(commit),才數得出「最後 commit 與最新 release 差多少」。
  session 起點=transcript 第一列時間戳(同 countdown.py 的基準)。
- 「該不該出貨」=有沒有動到知識(wiki/docs/_archive)以外的檔。只改 wiki/docs 不擋。
- 「哪些 repo 不適用」=repo 根目錄有 .isep-release-exempt(自帶理由,非中央手列清單,
  新 repo 預設納管)。
- 「這次先不出」=.claude/release-holds.md 寫一行理由(放行且留痕;形狀同 branch-holds.md,
  是「要求某個東西在場」而非文字層封路)。
「出貨」=git tag --contains HEAD 非空(Gitea release 就是 tag,本機答得準、不打網路)。

只掛 Stop、不掛 SubagentStop:subagent 交回分支、release 由總管出,逼 subagent 出貨純屬誤攔。
fail-open:拿不到 transcript 起點/非 git/git 問不出來一律放行(誤攔比漏擋嚴重)。

盤點:61→62 支、84→85 條(在自己的樹上實數,未加減推算);未掛上仍 3 支(不變)。
測試:scripts/test-unreleased-police.sh 13/13(該擋/不該擋/出路走得通/鑑別力四群,全離線)。
docs/hooks-inventory.md F 組加一列+版本註記;docs/TESTING.md A27。

plugin.json 待總管定版:版本號與 tag 由總管在打 release 那一刻同 commit 改(升版是傳輸
機制本身,check-version-consistency 要求 version==最新 tag,故不在分支上先動)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6AcoT4ALtPLsoCKab6pXt
2026-09-03 21:06:13 +00:00

103 lines
5.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# unreleased-police.sh 的迴歸測試(inkstone/ISEP#68docs/TESTING.md A27
#
# 兩個方向都要有證據(本 repo 心法:誤攔比漏擋嚴重):
# A 群「該擋」 —— session 改了 code 卻沒出貨(含「從未出過版」與「領先最新 release N 筆」)
# B 群「不該擋」 —— 只讀不寫/只改 wiki/已出貨(tag 收進 HEAD)/排除 marker/這次先不出/stop_hook_active
# C 群「出路走得通」—— 照擋下訊息教的「寫 release-holds.md 一行」做,再送一次就放行(ISEP#112 的課)
# D 群「鑑別力」 —— 拿掉 marker 同一個 repo 就會擋(證明放行是 marker 造成的,不是偵測壞了)
#
# 🔴 全程在乾淨的 TMP 底下造假 repo/假 transcript,跑完自己清;不碰任何真 repo、不打網路。
set -u
HOOK="${1:-$(cd "$(dirname "$0")/.." && pwd)/hooks/unreleased-police.sh}"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# session 起點固定成 2026-01-01T00:00:00Ztranscript 第一列的時間戳)。
# 「這個 session 的 commit」=committer-date 晚於這一刻。
T0="2026-01-01T00:00:00Z"
TR="$TMP/transcript.jsonl"
printf '{"timestamp":"%s","type":"user","message":{"content":[{"type":"text","text":"開工"}]}}\n' "$T0" > "$TR"
AFTER="2026-06-01T00:00:00Z" # 晚於起點 = 這個 session 改的
BEFORE="2025-06-01T00:00:00Z" # 早於起點 = 不是這個 session 改的
PASS=0; FAIL=0; N=0
mkrepo() { git init -q "$1"; git -C "$1" config user.email t@t; git -C "$1" config user.name t; }
# commit <repo> <date> <相對路徑> <訊息>
commit() {
mkdir -p "$(dirname "$1/$3")"; echo x >> "$1/$3"; git -C "$1" add -A
GIT_AUTHOR_DATE="$2" GIT_COMMITTER_DATE="$2" git -C "$1" commit -qm "$4"
}
# fire <說明> <期望exit> <repo目錄> [extra-json] [--grep <字串>]
fire() {
desc="$1"; want="$2"; repo="$3"; extra="${4:-}"; needle="${6:-}"
N=$((N+1))
payload=$(printf '{"session_id":"s%s","transcript_path":"%s"%s}' "$N" "$TR" "$extra")
out=$(printf '%s' "$payload" | CLAUDE_PROJECT_DIR="$repo" bash "$HOOK" 2>&1); rc=$?
ok=1
[ "$rc" = "$want" ] || ok=0
if [ -n "$needle" ] && ! printf '%s' "$out" | grep -qF "$needle"; then ok=0; fi
if [ "$ok" = 1 ]; then
printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
else
printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s' "$desc" "$want" "$rc"
[ -n "$needle" ] && printf '(找 "%s"' "$needle"
printf '\n'
printf '%s\n' "$out" | sed -n '1,10p' | sed 's/^/ /'
FAIL=$((FAIL+1))
fi
}
echo "── A 群:該擋(改了 code 卻沒出貨)──"
mkrepo "$TMP/a1"; commit "$TMP/a1" "$AFTER" hooks/foo.sh "加一支 hook"
fire "改了 hook、從未出過版 → 擋,且點名 repo+筆數" 2 "$TMP/a1" "" --grep "從未出過版"
mkrepo "$TMP/a2"; commit "$TMP/a2" "$BEFORE" hooks/a.sh a; git -C "$TMP/a2" tag v1
commit "$TMP/a2" "$AFTER" hooks/b.sh b
fire "有舊 release、新 commit 未收進 → 擋,訊息說『領先它 N 筆』" 2 "$TMP/a2" "" --grep "領先它 1 筆"
mkrepo "$TMP/a3"; commit "$TMP/a3" "$AFTER" scripts/x.sh x; commit "$TMP/a3" "$AFTER" system-dev/wiki/n.md n
fire "同一 session 改了 code+wiki → 擋(只要有一個非知識檔)" 2 "$TMP/a3" "" --grep "未出貨"
echo "── B 群:不該擋 ──"
mkrepo "$TMP/b1"; commit "$TMP/b1" "$BEFORE" hooks/foo.sh old
fire "這個 session 什麼都沒 commit(只讀不寫)→ 放行" 0 "$TMP/b1"
mkrepo "$TMP/b2"; commit "$TMP/b2" "$AFTER" system-dev/wiki/note.md "只改 wiki"
fire "這個 session 只改了 wiki → 放行" 0 "$TMP/b2"
mkrepo "$TMP/b2b"; commit "$TMP/b2b" "$AFTER" docs/hooks-inventory.md "只改 docs"
fire "這個 session 只改了 docs → 放行" 0 "$TMP/b2b"
mkrepo "$TMP/b3"; commit "$TMP/b3" "$AFTER" hooks/foo.sh code; git -C "$TMP/b3" tag v9
fire "改了 code 而且已出貨(tag 收進 HEAD)→ 放行" 0 "$TMP/b3"
mkrepo "$TMP/b4"; commit "$TMP/b4" "$AFTER" hooks/foo.sh code
printf '產物 repo,管線產出,不出版\n' > "$TMP/b4/.isep-release-exempt"
fire "改了 code 但有 .isep-release-exempt marker → 放行" 0 "$TMP/b4"
mkrepo "$TMP/b5"; commit "$TMP/b5" "$AFTER" hooks/foo.sh code
mkdir -p "$TMP/b5/.claude"; printf 'b5: 交回分支,release 由總管出\n' > "$TMP/b5/.claude/release-holds.md"
fire "改了 code 沒出貨、但 release-holds.md 有這個 repo(先不出)→ 放行且留痕" 0 "$TMP/b5"
mkrepo "$TMP/b6"; commit "$TMP/b6" "$AFTER" hooks/foo.sh code
fire "已被別的 Stop 閘擋過這一輪(stop_hook_active)→ 放行" 0 "$TMP/b6" ',"stop_hook_active":true'
echo "── C 群:擋下訊息教的出路,照做真的走得通 ──"
mkrepo "$TMP/c1"; commit "$TMP/c1" "$AFTER" hooks/foo.sh code
fire "先擋一次" 2 "$TMP/c1" "" --grep "release-holds.md"
# 照訊息說的:在 .claude/release-holds.md 寫「<repo 名>: <理由>」
mkdir -p "$TMP/c1/.claude"; printf 'c1: 這次累積,下個 session 一起出\n' > "$TMP/c1/.claude/release-holds.md"
fire "照訊息寫了 release-holds.md 那一行,再送 → 放行" 0 "$TMP/c1"
echo "── D 群:鑑別力(放行是 marker/hold 造成的,不是偵測壞了)──"
# b4 若拿掉 marker,同一個 repo 就會擋 → 證明偵測本身是會亮的
rm -f "$TMP/b4/.isep-release-exempt"
fire "把 b4 的 marker 拿掉 → 同一個 repo 就擋(偵測真的會亮)" 2 "$TMP/b4"
echo
echo "$PASS/$N 通過"
[ "$FAIL" -eq 0 ]