#!/bin/bash
# 自测 .claude/hooks/stop-figma-conformance.sh —— 必拦 / 必放 两侧都验。
#
# ⛔ 为什么必须有这个文件：那条 hook 的判据是「该挡的时候挡、该放的时候放」，
#    而**放**那一半在真 session 里看不出来（放行就是什么都没发生）。
#    没有阴性对照的 hook，等于只验过它会打扰人，没验过它不会乱打扰人。
#
# ⚠️ 如实登记的天花板：本文件验的是**脚本逻辑**。
#    「Claude Code 真的会在收尾时触发 Stop hook 并尊重 exit 2」⛔ 无法在此验证 ——
#    那要一次真 session。见 ai-ds-lab decision-queue Q6 里登记的边界。
#
# 用法：bash .claude/hooks/stop-figma-conformance.selftest.sh
set -u
HOOK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/stop-figma-conformance.sh"
PASS=0
FAIL=0

# ⚠️ 结果通过**全局** EC / ERRFILE / OUTFILE 回传，⛔ 不用 `$(run_hook …)` ——
#    命令替换是子 shell，里面的赋值传不回父（本文件初版就是这么写的，11 条用例
#    全部因 `ERRFILE: unbound variable` 假失败。⇒ 记在这里，别再写回去）。
EC=0
ERRFILE=""
OUTFILE=""
run_hook() { # $1 = session_id
  ERRFILE=$(mktemp)
  OUTFILE=$(mktemp)
  printf '{"session_id":"%s","hook_event_name":"Stop"}' "$1" \
    | TVU_STOP_CONFORMANCE_WAIT="${WAIT_OVERRIDE:-2}" \
      TVU_STOP_CONFORMANCE_MAX_BLOCKS="${MAX_OVERRIDE:-3}" \
      bash "$HOOK" > "$OUTFILE" 2> "$ERRFILE"
  EC=$?
}

check() { # $1 = 名称 ; $2 = 条件结果(0/1)
  if [ "$2" -eq 0 ]; then
    PASS=$((PASS + 1)); echo "  ✓ $1"
  else
    FAIL=$((FAIL + 1)); echo "  ✗ $1"
  fi
}

mkstate() { # $1 = session_id
  SD="${TMPDIR:-/tmp}/tvu-mockup-conformance/$1"
  rm -rf "$SD"; mkdir -p "$SD"; echo "$SD"
}

echo "▶ 自测 stop-figma-conformance.sh"

# ── MN1 本 session 没有 Figma 写（无状态目录）⇒ 必须静默放行 ─────────────────
# 🔴 这是最重要的阴性对照：它决定这条 hook 会不会打扰**所有**与 Figma 无关的 session。
S="selftest-none-$$"
rm -rf "${TMPDIR:-/tmp}/tvu-mockup-conformance/$S"
run_hook "$S"
check "MN1 无状态目录 ⇒ exit 0" "$([ "$EC" -eq 0 ] && echo 0 || echo 1)"
check "MN1′ 且 stderr 为空（⛔ 不许兜底提醒）" "$([ ! -s "$ERRFILE" ] && echo 0 || echo 1)"

# ── MN2 状态目录在但既无 pending 也无未消费结果 ⇒ 该看的都看过了，放行 ────────
S="selftest-empty-$$"; SD=$(mkstate "$S")
run_hook "$S"
check "MN2 空状态目录 ⇒ exit 0" "$([ "$EC" -eq 0 ] && echo 0 || echo 1)"
check "MN2′ 且 stderr 为空" "$([ ! -s "$ERRFILE" ] && echo 0 || echo 1)"

# ── MH1 有未消费的结果 ⇒ 必须 exit 2 并把结果原文带出来 ──────────────────────
S="selftest-result-$$"; SD=$(mkstate "$S")
printf 'exit=0\n  ❌ FINDINGS mockup-integrity\n     · SELFTEST-PROBE-FINDING\n' > "$SD/result.txt"
run_hook "$S"
check "MH1 有未消费结果 ⇒ exit 2" "$([ "$EC" -eq 2 ] && echo 0 || echo 1)"
check "MH1′ 且 stderr 含结果原文" "$(grep -q 'SELFTEST-PROBE-FINDING' "$ERRFILE" && echo 0 || echo 1)"
check "MH1″ 且点明是「最后一轮」（⛔ 不是笼统提醒）" "$(grep -q '最后一轮' "$ERRFILE" && echo 0 || echo 1)"
check "MH1‴ 且带判读纪律（exit=0 ≠ 验过了）" "$(grep -q 'exit=0' "$ERRFILE" && echo 0 || echo 1)"

# ── MH2 消费一次：同一份结果 ⛔ 不得挡第二次（防循环 belt 1）──────────────────
check "MH2 result.txt 已被消费" "$([ ! -f "$SD/result.txt" ] && [ -f "$SD/result.consumed.txt" ] && echo 0 || echo 1)"
run_hook "$S"
check "MH2′ 第二次 Stop ⇒ exit 0（⛔ 不得无限挡）" "$([ "$EC" -eq 0 ] && echo 0 || echo 1)"

# ── MH3 有还在跑的 run ⇒ 等到上限后 exit 2 并说清「还在跑」──────────────────
S="selftest-pending-$$"; SD=$(mkstate "$S")
sleep 30 & SLEEP_PID=$!
echo "$SLEEP_PID" > "$SD/pending.pid"
run_hook "$S"
check "MH3 pending 未完 ⇒ exit 2" "$([ "$EC" -eq 2 ] && echo 0 || echo 1)"
check "MH3′ 且说清是「还在跑」而非「有结果」" "$(grep -q '还在跑' "$ERRFILE" && echo 0 || echo 1)"
check "MH3″ 且印出 report 落点供人自取" "$(grep -q "$SD" "$ERRFILE" && echo 0 || echo 1)"

# ── MH4 有界升级：达上限后必须放行，⛔ 不许把 session 卡死 ────────────────────
echo 3 > "$SD/stop-blocks"
MAX_OVERRIDE=3 run_hook "$S"
check "MH4 达阻塞上限 ⇒ exit 0（有界，⛔ 不无限等）" "$([ "$EC" -eq 0 ] && echo 0 || echo 1)"
check "MH4′ 且放行时留下响亮提示（⛔ 别静默放）" "$(grep -q 'systemMessage' "$OUTFILE" && echo 0 || echo 1)"
check "MH4″ 且逐字写明「放行不代表验过」" "$(grep -q '别把这次收尾读作' "$OUTFILE" && echo 0 || echo 1)"
kill "$SLEEP_PID" 2>/dev/null

# ── MN3 计数器文件存在但为空 ⇒ ⛔ 不得报错（本仓已登记的同族坑）──────────────
S="selftest-emptyctr-$$"; SD=$(mkstate "$S")
: > "$SD/stop-blocks"
printf 'exit=0\n  ✅ PASS all\n' > "$SD/result.txt"
run_hook "$S"
check "MN3 空计数器 ⇒ 不崩（exit 2 而非 shell 报错）" "$([ "$EC" -eq 2 ] && echo 0 || echo 1)"
check "MN3′ 且 stderr 里⛔ 没有 shell 的 integer 报错" "$(! grep -qiE 'integer expression|not found|unary operator' "$ERRFILE" && echo 0 || echo 1)"

# ── MN4 pending.pid 指向**已死**的进程 ⇒ ⛔ 不得白等，必须继续看结果 ──────────
S="selftest-deadpid-$$"; SD=$(mkstate "$S")
sleep 0.1 & DEAD=$!; wait "$DEAD" 2>/dev/null
echo "$DEAD" > "$SD/pending.pid"
printf 'exit=0\n  ❌ FINDINGS DEADPID-PROBE\n' > "$SD/result.txt"
start=$(date +%s)
WAIT_OVERRIDE=20 run_hook "$S"
elapsed=$(( $(date +%s) - start ))
check "MN4 死 pid ⇒ 不白等（<5s）" "$([ "$elapsed" -lt 5 ] && echo 0 || echo 1)"
check "MN4′ 且仍取到结果" "$(grep -q 'DEADPID-PROBE' "$ERRFILE" && echo 0 || echo 1)"

# ── MH5 写了却一次机检都没跑（post-figma-write 落的 marker）⇒ 挡一次，点出次数 ──
S="selftest-unchecked-$$"; SD=$(mkstate "$S")
printf '1757000001 no-filekey\n1757000002 no-filekey\n1757000003 no-repo\n' > "$SD/unchecked-writes"
run_hook "$S"
check "MH5 有未机检的写 ⇒ exit 2" "$([ "$EC" -eq 2 ] && echo 0 || echo 1)"
check "MH5′ 且点出次数 3" "$(grep -q '3 次' "$ERRFILE" && echo 0 || echo 1)"
check "MH5″ 且 marker 已消费" "$([ ! -f "$SD/unchecked-writes" ] && [ -f "$SD/unchecked-writes.consumed" ] && echo 0 || echo 1)"
run_hook "$S"
check "MH5‴ 第二次 Stop ⇒ exit 0（消费一次，⛔ 不得无限挡）" "$([ "$EC" -eq 0 ] && echo 0 || echo 1)"

# ── MN5 marker 文件存在但为空 ⇒ 不挡、不崩（与 MN3 同族）────────────────────────
S="selftest-emptyunchecked-$$"; SD=$(mkstate "$S")
: > "$SD/unchecked-writes"
run_hook "$S"
check "MN5 空 marker ⇒ exit 0" "$([ "$EC" -eq 0 ] && echo 0 || echo 1)"
check "MN5′ 且 stderr 为空" "$([ ! -s "$ERRFILE" ] && echo 0 || echo 1)"

for s in none empty result pending emptyctr deadpid unchecked emptyunchecked; do rm -rf "${TMPDIR:-/tmp}/tvu-mockup-conformance/selftest-$s-$$"; done

echo "▶ 自测：$PASS 过 / $FAIL 失败"
[ "$FAIL" -eq 0 ] || exit 1
exit 0
