da9bd53cae
兩張票放同一條分支:都是「該發生卻不會自己發生的事」,觸發點都在 session 邊界。 ## inkstone/ISEP#93 —— 逾期和掛著沒人接的事會主動叫 - scripts/isep-nag 撈三種沒人會叫的事(逾期 milestone/等 leo 的票/掉在地上的棒子) - scripts/isep-notify 發 Telegram,而且**發不出去的時候不會安靜** - hooks/overdue-nag-guard.sh SessionStart 跑一次(不輪詢、不 fan-out、不掛 Actions) - hooks/tests/overdue-nag.test.sh 35 條,全程離線 實跑撈得出票上點名的那五個逾期 milestone(08-24 三個、08-26 兩個)。 沒東西可報時會說「查過了,沒有」——安靜跟壞掉長得一模一樣。 🔴 工單補的那個限制(今天實測出來的)已經處理: 「能不能發得出去」取決於這個 session 載到的 ISEP 是哪一版 (prod-write-guard v0.10.0 才認得出 notify_leo 不是部署,而 hook 註冊路徑 在 session 啟動當下就寫死了)。所以 isep-notify 會**先拿真的要送的那一則 去問這個 session 註冊的那支閘**,把判定寫成檔(誰都查得到),然後: · 放行 ⇒ 送,並驗內層 data.data.ok(外層 200 不算送到) · 會擋 ⇒ **不繞路**,改貼回票上並把原文印在眼前 用 git 歷史裡的真跡(c263866 那一版閘)測過「會擋」那條路。 ⛔ 實測發現通道本身現在是斷的:實例上找不到 notify_leo 工作流(404)。 不是閘、不是網路、不是金鑰。詳情與修法寫在 docs/TESTING.md 最後一段。 退路兩次都走通了(inkstone/ISEP#93 comment 5182、5184)。 ## inkstone/ISEP#89 —— wiki 太長時有人整理 - scripts/wiki-compress audit/plan/apply/verify/bench 五個動詞 - hooks/wiki-size-guard.sh SessionStart 點名太長的檔;寫檔時擋「沒走流程的壓縮」 - hooks/tests/wiki-compress.test.sh 25 條,全程離線、不碰真的 wiki 設計上最重要的一條:**只搬不改**,正文一個字都不動。 「合併同類、濃縮成一行」要重寫正文,而重寫的當下沒有人會發現弄丟了什麼。 所以機器只做「搬 + 目錄 + 標 ×N/↻」,合併留給人。 拿現在的 mistakes.md 複本實壓過(不動真的 wiki): 7,681 行 → 1,199 行 260 條一條都沒少(verify 用內文雜湊逐條對帳) 80 個查詢命中率 80/80,定位成本 2,956 → 131 行,**快 22.5 倍**(bench) verify 反向測過:真的弄丟一條時它抓得到(抓不到的對帳表比沒有更糟)。 ## 盤點數字 在這棵樹上當場數的,不是拿上一版加減推的: ls hooks/*.sh | wc -l → 55(was 53) grep -c '"command":' hooks.json → 71(was 68) plugin.json/README/hooks-inventory 三處同步改。 **版本號沒動**(0.11.0),待總管定版。
187 lines
10 KiB
Bash
Executable File
187 lines
10 KiB
Bash
Executable File
#!/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' ' ')"; }
|
||
|
||
BIG=$(python3 -c 'print("這是一段很長的舊內容。" * 200)')
|
||
SMALL="剩這一句。"
|
||
R=$(run "$(mk 1 Edit "$(python3 -c "
|
||
import json,sys
|
||
print(json.dumps({'file_path':'$W/mistakes.md','old_string':'''$BIG''','new_string':'$SMALL'}, ensure_ascii=False))")")")
|
||
case "$R" in 2\|*) ok "⑯ 砍掉一大段 wiki ⇒ 擋一次" ;; *) no "⑯ 該擋" "實得 $R" ;; esac
|
||
has "$R" "只搬不改" && ok "⑰ 擋的訊息說得出出路(三步指令)" || no "⑰ 出路" "$R"
|
||
|
||
R=$(run "$(mk 2 Edit "$(python3 -c "
|
||
import json
|
||
print(json.dumps({'file_path':'$W/mistakes.md','old_string':'這是一句舊的話,長度普通。','new_string':'這是一句新的話,長度也普通,改了幾個字。'}, ensure_ascii=False))")")")
|
||
case "$R" in 0\|*) ok "⑱ 一般編輯不擋(誤攔比漏擋更該修)" ;; *) no "⑱ 不該擋" "實得 $R" ;; esac
|
||
|
||
R=$(run "$(mk 3 Edit "$(python3 -c "
|
||
import json
|
||
print(json.dumps({'file_path':'/somewhere/src/app.ts','old_string':'x'*3000,'new_string':'y'}, ensure_ascii=False))")")")
|
||
case "$R" in 0\|*) ok "⑲ 不是 wiki 的檔不管(別的檔縮水是它們自己的事)" ;; *) no "⑲ 非 wiki" "實得 $R" ;; esac
|
||
|
||
R=$(run "$(mk 4 Edit "$(python3 -c "
|
||
import json,sys
|
||
print(json.dumps({'file_path':'$W/mistakes.md','old_string':'''$BIG''','new_string':'剩這一句。 wiki-compress-ok'}, ensure_ascii=False))")")")
|
||
case "$R" in 0\|*) ok "⑳ wiki-compress-ok 留痕式豁免走得通" ;; *) no "⑳ 豁免" "實得 $R" ;; esac
|
||
|
||
R=$(run "$(mk 5 Write "$(python3 -c "
|
||
import json
|
||
print(json.dumps({'file_path':'$W/從來沒有過的新檔.md','content':'新檔案的一點內容'}, ensure_ascii=False))")")")
|
||
case "$R" in 0\|*) ok "㉑ 新建檔案不算壓縮" ;; *) no "㉑ 新檔" "實得 $R" ;; esac
|
||
|
||
# 至多擋一次:同一個 session 連兩次,第二次要放行(不然會卡死)
|
||
PAY=$(mk 6 Edit "$(python3 -c "
|
||
import json
|
||
print(json.dumps({'file_path':'$W/mistakes.md','old_string':'''$BIG''','new_string':'剩這一句。'}, ensure_ascii=False))")")
|
||
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 ]
|