#!/bin/bash # 第八轮(补 chain6/7 未覆盖的两项证据): # A. npm run test:coverage —— A-7 阈值棘轮在最终树上是否仍成立 # B. iPhone SE 口径复跑(含新增的 404 边界页),证据写入新目录,不覆盖 09-21 的旧证据 set -u cd /Users/zhangxiang/Codes/Novalon/novalon-website || exit 1 LOG=/tmp/final-chain8.log SE_OUT=docs/acceptance/2026-09-22-iphone-se PORTS() { lsof -nP -iTCP:3000 -iTCP:3100 -sTCP:LISTEN 2>/dev/null | grep -c LISTEN; } echo "=== chain8 started $(date -u +%FT%TZ) ===" > "$LOG" while ! grep -q "chain7 done" /tmp/final-chain7.log 2>/dev/null; do sleep 20; done echo "CHAIN7_TAIL: $(grep -aE 'EXIT_AXE_FULL|AXE routeCount|EXIT_LIGHTHOUSE|CHAIN7_DEAD|^crawled|EXIT_SANITY' /tmp/final-chain7.log | tr '\n' '|')" >> "$LOG" echo "PREFLIGHT_LISTENERS=$(PORTS) $(date -u +%FT%TZ)" >> "$LOG" npm run test:coverage > /tmp/coverage-final8.log 2>&1 echo "EXIT_COVERAGE=$? $(date -u +%FT%TZ)" >> "$LOG" { echo "# test:coverage 最终树复跑 $(date -u +%FT%TZ)" sed -e 's/\x1b\[[0-9;]*m//g' /tmp/coverage-final8.log | grep -aE "All files|Jest:|Tests:|Test Suites:|threshold|% (Stmts|Branch)" | head -20 } > docs/acceptance/2026-09-21-gates/coverage-final-tree.txt 2>&1 sleep 2 echo "POST_COVERAGE_LISTENERS=$(PORTS)" >> "$LOG" mkdir -p "$SE_OUT" npx next start -p 3000 > /tmp/se-server.log 2>&1 & SRV=$! code=000 for i in $(seq 1 40); do code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/ 2>/dev/null || true) [ "$code" = "200" ] && break sleep 3 done echo "SE_SERVER_UP=$code $(date -u +%FT%TZ)" >> "$LOG" BASE=http://localhost:3000 OUT="$SE_OUT" \ node --input-type=module -e "$(cat docs/acceptance/2026-09-21-iphone-se/iphone-se-audit.mjs)" \ > "$SE_OUT/audit.log" 2>&1 echo "EXIT_SE_AUDIT=$? $(date -u +%FT%TZ)" >> "$LOG" kill "$SRV" 2>/dev/null pkill -f 'next start -p 3000' 2>/dev/null sleep 3 node -e ' const j = require("./'"$SE_OUT"'/measurements.json"); const pending = j.rows.filter((r) => /pending/.test(r.tag)); const overlap = pending.filter((r) => r.bannerOverlapsFooter); const nf = j.rows.filter((r) => /not-found/.test(r.tag)); const bad = j.rows.filter((r) => r.docScrollW > r.clientW || r.theme === null); console.log("SE rows=" + j.rows.length + " overflow=" + j.rows.filter((r) => r.docScrollW > r.clientW).length + " themeNull=" + j.rows.filter((r) => r.theme === null).length + " pendingRows=" + pending.length + " pendingBannerOverlapFooter=" + overlap.length + " notFoundRows=" + nf.length + " notFoundGaps=" + JSON.stringify(nf.map((r) => r.footerBottomGap)) + " footerGaps=" + JSON.stringify([...new Set(j.rows.map((r) => r.footerBottomGap))])); console.log("SE PASSED =", j.rows.length === 20 && bad.length === 0); ' >> "$LOG" 2>&1 echo "POST_FINAL_LISTENERS=$(PORTS)" >> "$LOG" echo "=== chain8 done $(date -u +%FT%TZ) ===" >> "$LOG"