Files
ISEP/hooks/tests/wiki-compress.test.sh
T
Leo 5173c7f3e5 fix(tests):修好三支跟工具無關的壞測試(inkstone/ISEP#161)
三支測試在乾淨 main 上一直紅,全部是測試自己的 bug,不是工具的離開碼漂了:

1. hooks/tests/wiki-compress.test.sh(⑯⑰㉒):E 群拼 payload 時把
   {'file_path':...} 直接嵌進三層巢狀 "$(...)" 裡;其中一層雙引號提早收口,
   讓大括號在 bash 眼裡變成「未加引號」,觸發大括號展開——逗號分隔的
   內容被拆成三條各自獨立的 python3 -c 呼叫,`{`/`}` 本身被吃掉,
   python 因此噴三次 SyntaxError。改用兩支小函式(json_edit/json_write)
   把內容經環境變數傳給 python,內容永遠不會被當成 bash 原始碼解析。

2. scripts/test-stage.sh(A1–A4、D1–D4、D14):全部卡在
   `[ "$(wc -l < "$LOG")" = 0 ]`——macOS/BSD 的 `wc -l` 會在數字前墊空白
   (GNU coreutils 不會),比對永遠是 "       0" != "0"。工具本身的離開碼
   是對的(rc=2,正確拒絕),是測試的比對方式在這台機器上有 bug。
   一律加 `| tr -d " "` 去掉墊白。

3. scripts/test-milestone-account.sh(⑰):同一款 macOS `wc -l` 墊白 bug,
   帳本筆數比對 `[ "$(wc -l < ...)" = "4" ]` 永遠比不過。同樣修法。

三支都在 `env -u CLAUDE_CODE_CHILD_SESSION` 下重跑到 exit 0:
  wiki-compress.test.sh        25/25 通過
  test-stage.sh                 45/45 通過(連跑 3 次穩定)
  test-milestone-account.sh     23/23 通過

跑過 docs/TESTING.md 記載的其餘 52 支測試(不含兩支需要真 Claude CLI 的
live 測試),全數 0 失敗,唯一例外是 hooks/tests/mainline-members-by-dependency.test.sh
的 ㊽(帳本 cwd 形狀)——在乾淨的 main(未套用本次修改)上同樣紅,
與本次改動的三個檔案無關,非本票範圍,另行處理。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 13:32:59 +08:00

200 lines
11 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
# wiki 壓縮的迴歸測試(inkstone/ISEP#89
#
# 對應票上那三條驗收:
# 1 壓完之後同一件事查得到、而且查得比壓之前快 → C 群(bench)
# 2 壓縮前後不能弄丟任何一條教訓(逐條對帳) → B 群(verify
# 3 壓縮這件事本身要有票、有交付紀錄 → D 群(--ticket .compress-log.md
#
# 另外兩群是這套的主要失效模式:
# A 群 切條要切對 —— 切錯的話後面三件全部失準(尤其 code fence 裡的 `#`
# E 群 閘:大幅刪減要擋、正常編輯不准擋 —— **誤攔比漏擋更該修**
#
# 🔴 全程離線、只在 mktemp 目錄裡動檔案,不碰任何真的 wiki。
set -u
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
WC="$ROOT/scripts/wiki-compress"
HOOK="$ROOT/hooks/wiki-size-guard.sh"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP" /tmp/.wiki-size-guard-wctest*' EXIT
# 🔴 目錄結構要跟真的一樣(`<專案>/system-dev/wiki`),不然 F 群測到的是
# 「hook 找不到 wiki 目錄所以閉嘴」,那是假綠——它跟「都在門檻內」長得一模一樣。
W="$TMP/system-dev/wiki"; mkdir -p "$W"
PASS=0; FAIL=0
ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
no(){ printf ' ❌ %s —— %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
has(){ printf '%s' "$1" | grep -q -- "$2"; }
# ── 測資:一份小而完整的假 wiki(含所有會出事的形狀)──────────────────
{
echo "# 假的 mistakes"
echo
echo "> 前言,壓縮後要留在最上面。"
echo
for i in $(seq 1 40); do
d=$(printf '2026-0%d-%02d' $(( (i % 3) + 6 )) $(( (i % 27) + 1 )))
echo
echo "## 第 $i 條:獨特關鍵詞 KEY$i 的教訓($d"
echo
echo "**症狀**:某某東西壞了 $i。票:inkstone/ISEP#$((100 + i))"
echo
echo '```sh'
echo "# 這一行是程式碼註解,不是標題 $i"
echo "## 這一行也是,切條的時候不准把它當成一條"
echo '```'
echo
for _ in $(seq 1 12); do echo "正文填充 $i:這一段是為了讓每條有足夠的行數。"; done
done
echo
echo "## 自承重複的那一條:這是同款第三次(2026-08-20"
echo
echo "同一個病又犯一次。票:inkstone/ISEP#999"
} > "$W/mistakes.md"
cp "$W/mistakes.md" "$TMP/orig.md"
echo "── A 群:切條要切對(切錯的話後面三件全部失準)──────────────────"
A=$(python3 "$WC" audit "$W" 2>&1)
has "$A" "41 條" && ok "① 切出 41 條(40 自承重複那條)" \
|| no "① 切出 41 條" "實得:$(printf '%s' "$A" | grep mistakes)"
has "$A" "這一行也是" && no "② code fence 裡的 ## 不算一條" "算進去了" \
|| ok "② code fence 裡的 ## 不算一條(mistakes.md 真的有這種行)"
has "$A" "自承是同款" && ok "③ 認得出「自承是同款第 N 次」(人下的判斷比機器猜的準)" \
|| no "③ 自承重複" "沒偵測到"
echo "── B 群:不能弄丟任何一條(票上驗收 2)──────────────────────────"
AP=$(WIKI_COMPRESS_THRESHOLD=400 python3 "$WC" apply "$W/mistakes.md" \
--ticket inkstone/ISEP#89 2>&1)
has "$AP" "✅ 壓完了" && ok "④ 壓得動" || no "④ 壓得動" "$AP"
V=$(python3 "$WC" verify "$W/mistakes.md.before-compress" "$W/mistakes.md" \
"$W"/mistakes-archive-*.md 2>&1)
has "$V" "一條都沒弄丟" && ok "⑤ 逐條對帳:一條都沒弄丟" || no "⑤ 逐條對帳" "$V"
diff -q "$TMP/orig.md" "$W/mistakes.md.before-compress" >/dev/null \
&& ok "⑥ 壓縮前的原檔完整留著(.before-compress,錯了還原得回去)" \
|| no "⑥ 原檔留底" "跟壓縮前不一樣"
# 真的弄丟一條 ⇒ verify 一定要抓到。**抓不到的對帳表比沒有更糟。**
cp "$W/mistakes.md" "$TMP/tampered.md"
(python3 - "$W" "$TMP/tampered.md" <<'PY'
import glob, os, re, sys
w, tam = sys.argv[1], sys.argv[2]
# 從某個 archive 裡整條刪掉一條,模擬「壓縮的時候弄丟了」
for f in sorted(glob.glob(os.path.join(w, "mistakes-archive-*.md"))):
t = open(f, encoding="utf-8").read()
parts = re.split(r"(?m)^## ", t)
if len(parts) > 2:
open(f + ".broken", "w", encoding="utf-8").write(
parts[0] + "## " + "## ".join(parts[2:]))
print(f + ".broken")
break
PY
) >/dev/null
BROKEN=$(ls "$W"/mistakes-archive-*.md.broken 2>/dev/null | head -1)
if [ -n "$BROKEN" ]; then
KEEP=$(ls "$W"/mistakes-archive-*.md 2>/dev/null | grep -v '\.broken$' \
| grep -v "$(basename "${BROKEN%.broken}")" || true)
# shellcheck disable=SC2086
V2=$(python3 "$WC" verify "$W/mistakes.md.before-compress" "$W/mistakes.md" \
"$BROKEN" $KEEP 2>&1); RC2=$?
has "$V2" "弄丟了" && [ "$RC2" = 1 ] \
&& ok "⑦ **真的弄丟一條時 verify 抓得到**(反向測過,離開碼 1)" \
|| no "⑦ 反向測" "沒抓到(rc=$RC2):$(printf '%s' "$V2" | tr '\n' ' ')"
else
printf ' ⏭️ SKIP ⑦:這次沒有 archive 可以拿來破壞\n'
fi
echo "── C 群:查得到,而且查得更快(票上驗收 1)──────────────────────"
B=$(python3 "$WC" bench "$W/mistakes.md.before-compress" "$W/mistakes.md" \
"$W"/mistakes-archive-*.md 2>&1)
has "$B" "查得更快" && ok "⑧ 命中率沒掉,而且更快" || no "⑧ 更快" "$B"
BEF=$(printf '%s' "$B" | grep -o '壓縮前.*平均 [0-9]* 行' | grep -o '[0-9]*' | tail -1)
AFT=$(printf '%s' "$B" | grep -o '壓縮後.*平均 [0-9]* 行' | grep -o '[0-9]*' | tail -1)
[ -n "$BEF" ] && [ -n "$AFT" ] && [ "$AFT" -lt "$BEF" ] \
&& ok "⑨ 定位成本真的變小($BEF$AFT 行)" \
|| no "⑨ 定位成本" "前=$BEF 後=$AFT"
has "$(cat "$W/mistakes.md")" "KEY7" && ok "⑩ 壓完的原檔裡查得到舊條目(目錄留了指標)" \
|| no "⑩ 目錄留指標" "KEY7 在壓縮後的原檔裡不見了"
echo "── D 群:壓縮這件事本身要有票、有交付紀錄(票上驗收 3)────────────"
NT=$(python3 "$WC" apply "$W/mistakes.md" 2>&1); RCN=$?
[ "$RCN" != 0 ] && ok "⑪ 沒給 --ticket 就不准壓" || no "⑪ 沒票不准壓" "居然壓了"
BAD=$(python3 "$WC" apply "$W/mistakes.md" --ticket 89 2>&1); RCB=$?
[ "$RCB" != 0 ] && ok "⑫ 裸號不算票號(跨 repo 會撞號)" || no "⑫ 裸號" "收了"
[ -f "$W/.compress-log.md" ] && ok "⑬ 留下交付紀錄 .compress-log.md" \
|| no "⑬ 交付紀錄" "沒有這個檔"
L=$(cat "$W/.compress-log.md" 2>/dev/null)
has "$L" "inkstone/ISEP#89" && ok "⑭ 紀錄裡寫著票號(誰批准壓的查得到)" || no "⑭ 票號" "$L"
has "$L" "對帳指令" && ok "⑮ 紀錄裡寫著對帳指令(下一個人跑得起來)" || no "⑮ 對帳指令" "$L"
echo "── E 群:閘——大幅刪減要擋,正常編輯不准擋 ──────────────────────"
mk(){ printf '{"hook_event_name":"PreToolUse","session_id":"wctest%s","tool_name":"%s","tool_input":%s}' "$1" "$2" "$3"; }
run(){ rm -f /tmp/.wiki-size-guard-wctest*; out=$(printf '%s' "$1" | bash "$HOOK" 2>&1); printf '%s|%s' "$?" "$(printf '%s' "$out" | tr '\n' ' ')"; }
# 🔴 這兩支輔助函式的存在理由(inkstone/ISEP#161):舊版把 $BIG$SMALL 直接嵌進
# `python3 -c "..."` 的原始碼字串裡,而外層又套了三層 "$(...)"。當中一層雙引號
# 提早收口,讓 `{'file_path':...}` 這段在 bash 眼裡變成「未加引號」,觸發**大括號展開**
# (逗號分隔 → 拆成三條各自獨立的 python3 -c 呼叫,`{``}` 本身被吃掉)——
# 症狀就是 payload 少了 dict 的大括號、且 python 噴三次 SyntaxError。
# 改用環境變數把內容傳給 python:內容永遠不會被當成 bash 原始碼解析,
# 也不必再猜三層引號怎麼配對。
json_edit(){ # file_path old_string new_string
PL_FILE="$1" PL_OLD="$2" PL_NEW="$3" python3 -c '
import json, os
print(json.dumps({
"file_path": os.environ["PL_FILE"],
"old_string": os.environ["PL_OLD"],
"new_string": os.environ["PL_NEW"],
}, ensure_ascii=False))'
}
json_write(){ # file_path content
PL_FILE="$1" PL_CONTENT="$2" python3 -c '
import json, os
print(json.dumps({
"file_path": os.environ["PL_FILE"],
"content": os.environ["PL_CONTENT"],
}, ensure_ascii=False))'
}
BIG=$(python3 -c 'print("這是一段很長的舊內容。" * 200)')
SMALL="剩這一句。"
R=$(run "$(mk 1 Edit "$(json_edit "$W/mistakes.md" "$BIG" "$SMALL")")")
case "$R" in 2\|*) ok "⑯ 砍掉一大段 wiki ⇒ 擋一次" ;; *) no "⑯ 該擋" "實得 $R" ;; esac
has "$R" "只搬不改" && ok "⑰ 擋的訊息說得出出路(三步指令)" || no "⑰ 出路" "$R"
R=$(run "$(mk 2 Edit "$(json_edit "$W/mistakes.md" "這是一句舊的話,長度普通。" "這是一句新的話,長度也普通,改了幾個字。")")")
case "$R" in 0\|*) ok "⑱ 一般編輯不擋(誤攔比漏擋更該修)" ;; *) no "⑱ 不該擋" "實得 $R" ;; esac
R=$(run "$(mk 3 Edit "$(json_edit "/somewhere/src/app.ts" "$(python3 -c "print('x' * 3000)")" "y")")")
case "$R" in 0\|*) ok "⑲ 不是 wiki 的檔不管(別的檔縮水是它們自己的事)" ;; *) no "⑲ 非 wiki" "實得 $R" ;; esac
R=$(run "$(mk 4 Edit "$(json_edit "$W/mistakes.md" "$BIG" "剩這一句。 wiki-compress-ok")")")
case "$R" in 0\|*) ok "⑳ wiki-compress-ok 留痕式豁免走得通" ;; *) no "⑳ 豁免" "實得 $R" ;; esac
R=$(run "$(mk 5 Write "$(json_write "$W/從來沒有過的新檔.md" "新檔案的一點內容")")")
case "$R" in 0\|*) ok "㉑ 新建檔案不算壓縮" ;; *) no "㉑ 新檔" "實得 $R" ;; esac
# 至多擋一次:同一個 session 連兩次,第二次要放行(不然會卡死)
PAY=$(mk 6 Edit "$(json_edit "$W/mistakes.md" "$BIG" "剩這一句。")")
rm -f /tmp/.wiki-size-guard-wctest6
printf '%s' "$PAY" | bash "$HOOK" >/dev/null 2>&1; first=$?
printf '%s' "$PAY" | bash "$HOOK" >/dev/null 2>&1; second=$?
[ "$first" = 2 ] && [ "$second" = 0 ] \
&& ok "㉒ 至多擋一次(被擋兩次的閘會被想辦法繞過)" \
|| no "㉒ 至多擋一次" "第一次 $first、第二次 $second"
echo "── F 群:SessionStart 講出來哪幾個檔沒人讀得完 ──────────────────"
S=$(printf '{"hook_event_name":"SessionStart","source":"startup"}' \
| CLAUDE_PROJECT_DIR="$TMP" WIKI_COMPRESS_THRESHOLD=50 bash "$HOOK" 2>/dev/null)
printf '%s' "$S" | python3 -c 'import json,sys;json.load(sys.stdin)' 2>/dev/null \
&& ok "㉓ 吐得出合法 JSON" || no "㉓ 合法 JSON" "實得:$(printf '%s' "$S" | head -c 200)"
has "$S" "沒人讀得完" && ok "㉔ 真的點名超過門檻的檔" || no "㉔ 點名" "$S"
S2=$(printf '{"hook_event_name":"SessionStart","source":"startup"}' \
| CLAUDE_PROJECT_DIR="$TMP" WIKI_COMPRESS_THRESHOLD=99999 bash "$HOOK" 2>/dev/null)
[ -z "$S2" ] && ok "㉕ 都在門檻內就閉嘴(沒事還講話=下次沒人看)" || no "㉕ 沒事閉嘴" "$S2"
echo
printf '通過 %d 條,失敗 %d 條\n' "$PASS" "$FAIL"
[ "$FAIL" = 0 ]