From 15f7dcc5fb12c18c0c3d690c152fe0a5f701c91c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 31 Aug 2026 13:41:25 +0800 Subject: [PATCH] =?UTF-8?q?test(smoke):=20=E6=8E=A5=E7=B7=9A=E8=85=B3?= =?UTF-8?q?=E6=9C=AC=E5=86=92=E7=85=99=E8=A3=9C=E4=B8=8A=E7=9B=B8=E4=BE=9D?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=AA=A2=E6=9F=A5=E7=9A=84=E5=8D=81=E4=B8=89?= =?UTF-8?q?=E6=A2=9D=E5=88=A4=E5=AE=9A=E6=96=B7=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接線腳本的 smoke 多一個沙箱,逐條跑相依版本檢查的判定路徑並比對結束碼: 相依落後的擋人與訊息內容、相依相等與超前的放行、豁免技能在相依落後時 照樣放行、同一個 plugin 底下非豁免技能照樣被擋、四種 fail-open、 逃生門蓋過相依落後,另加一條回歸——多行縮排的 manifest,jsc.requires 的最後一個鍵也要解得到。 原本的 hook 模式只驗那支腳本跑得完,相依版本這一段一條判定路徑都沒走到。 新的擋人情況判錯方向,不是把每一次技能呼叫鎖死,就是整道護欄形同虛設, 沒有斷言就看不出來。多行縮排是真實 manifest 的樣子,解析漏掉最後一個鍵 會讓落後的相依靜靜被放行,那一條非釘住不可。 沙箱自備一份暫時的 HOME,註冊檔路徑由 $HOME 決定,不覆寫就會讀到使用者 真正的安裝清單。假的 installed_plugins.json 與各 plugin 的 manifest 都放進 那份 HOME,註冊欄位故意寫成很新的版本、installPath 底下的 manifest 寫舊版, 把「只認 installPath 底下那份檔案」的規則一起釘住。GITEA_HOST 一律清空, 放行的案例才不會往下走到遠端比對真的連網。擋人的兩條另外比訊息字串, 比的是同一次執行留下的輸出。新增計數器 smoke_n_vg 與預期值 SMOKE_EXPECT_VG,一併納入總數、逐類比對訊息與結果摘要,判定路徑增減時 只改腳本裡的預期值。 所屬功能:版本前置檢查的相依版本閘門。 Co-Authored-By: Claude Opus 5 (1M context) --- tools/wire-cli.sh | 119 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 4 deletions(-) diff --git a/tools/wire-cli.sh b/tools/wire-cli.sh index 7dc3a86..d660c2a 100755 --- a/tools/wire-cli.sh +++ b/tools/wire-cli.sh @@ -22,6 +22,9 @@ # 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。 # write-guard.sh 的四種模式同法:階段鎖、稽核技能、提交指令與 release 解除各自的判定路徑, # 都用暫時的 $JSC_HOME 跑過一次並比對結束碼。 +# version-guard.sh 的相依版本檢查同法,只是沙箱換成暫時的 HOME(註冊檔路徑由 $HOME 決定): +# 相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、 +# 逃生門,再加一條回歸——多行縮排的 manifest,jsc.requires 的最後一個鍵也要解得到。 # # smoke 自己數結果行並自我斷言:`lines{數量}` 那一行印的是其後 `[jsc]` 結果行的實際條數, # 與腳本內宣告的預期條數逐類比對,不符就回非零。數字寫在腳本裡、由腳本自己印,散文引用那一行 @@ -743,7 +746,7 @@ if [ "$action" = smoke ]; then # 每一類實際跑過的結果行數。收尾時與下面宣告的預期條數逐類比對,加減判定路徑卻忘了改預期 # 就會當場失敗,散文與程式之間不會再各記一份數字。 - smoke_n_hook=0; smoke_n_model=0; smoke_n_wp=0; smoke_n_rs=0; smoke_n_wg=0 + smoke_n_hook=0; smoke_n_model=0; smoke_n_wp=0; smoke_n_rs=0; smoke_n_wg=0; smoke_n_vg=0 # 預期條數(改動判定路徑時一起改):每一類都要有自己的計數器,印得出結果行卻沒人計數的 # 那一類會讓總數永遠對不上,斷言也就形同虛設。 # hook 模式 九支 hook 的每個接線模式各一條。sdlc-gate.sh、comment-scope.sh、 @@ -752,11 +755,13 @@ if [ "$action" = smoke ]; then # 工作包 sdlc-gate.sh wp-check skill 的歸屬判定路徑 # 重啟閘門 restart-gate.sh 的判定、清除路徑與狀態檔範圍檢查 # 寫入閘門 write-guard.sh 三種擋人模式與 release 解除模式的判定路徑 + # 相依版本 version-guard.sh 讀 manifest jsc.requires 的擋人、放行、豁免與 fail-open 路徑 SMOKE_EXPECT_HOOK=17 SMOKE_EXPECT_MODEL=4 SMOKE_EXPECT_WP=6 SMOKE_EXPECT_RS=16 SMOKE_EXPECT_WG=21 + SMOKE_EXPECT_VG=13 # 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略) # $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。 @@ -1034,12 +1039,116 @@ if [ "$action" = smoke ]; then printf '[jsc] write-guard.sh:建不出暫存目錄,寫入與提交閘門判定沒驗到。\n' >> "$smoke_out" fi + # 相依版本檢查(version-guard.sh 的第二種擋人情況):上面那支只驗「hook 跑得完」, + # manifest 的 jsc.requires 一條判定路徑都沒走到。這裡自備一份暫時的 HOME,把假的 + # installed_plugins.json 與各 plugin 的 manifest 都放進去,逐條跑判定並比對結束碼。 + # 覆寫 HOME 是必要的:註冊檔路徑由 $HOME 決定,不覆寫就會讀到使用者真正的安裝清單, + # 冒煙結果變成看那台機器裝了哪幾版的臉色,也會在別人的安裝目錄上留下痕跡。 + # GITEA_HOST 一律清空:放行的案例會繼續往下走到遠端比對,站台推導得出來就會真的連網, + # 冒煙不該依賴網路,也不該讓遠端落後把預期放行的案例判成擋下。 + # 一律 &1); _rc=$? + smoke_n_vg=$((smoke_n_vg + 1)) + if [ "$_rc" -eq "$3" ]; then + printf '[jsc] version-guard.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] version-guard.sh(%s):exit %s,預期 %s,相依版本判定壞了:%s\n' \ + "$1" "$_rc" "$3" "$(printf '%s' "$vg_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + } + # 訊息內容也要比:擋下來卻沒指名是哪一個相依落後,使用者看不出要更新哪一個 plugin, + # 只看結束碼看不出這件事。比對的是上一個 smoke_vg_case 留下的那一次輸出。 + smoke_vg_msg() { # $1=情境 $2=預期字串 + smoke_n_vg=$((smoke_n_vg + 1)) + if printf '%s' "$vg_out" | grep -qF "$2"; then + printf '[jsc] version-guard.sh(%s):訊息含「%s」,與預期相同。\n' "$1" "$2" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] version-guard.sh(%s):訊息未含「%s」,落後的相依沒被指名:%s\n' \ + "$1" "$2" "$(printf '%s' "$vg_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + } + if vg_home=$(mktemp -d 2>/dev/null) \ + && mkdir -p "$vg_home/.claude/plugins" "$vg_home/p/sdlc" "$vg_home/p/cli" \ + "$vg_home/p/hooks" "$vg_home/p/git" "$vg_home/p/pkg" 2>/dev/null; then + vg_reg="$vg_home/.claude/plugins/installed_plugins.json" + vg_manifest="$vg_home/p/sdlc/plugin.json" + # 假的註冊檔:一個 domain 一列,閘門從這裡取 installPath。jsc-cli 那一列的 version 欄位 + # 故意寫成一個很新的版本,而它 installPath 底下的 manifest 寫 0.1.0——閘門只認後者, + # 拿註冊欄位當備援會放過真正被載入的舊版,這份資料就是把那條規則一起釘住。 + printf '{\n "jsc-sdlc@jsc": [{"name": "jsc-sdlc", "installPath": "%s/p/sdlc", "version": "0.1.0"}],\n "jsc-cli@jsc": [{"name": "jsc-cli", "installPath": "%s/p/cli", "version": "9.9.9"}],\n "jsc-hooks@jsc": [{"name": "jsc-hooks", "installPath": "%s/p/hooks", "version": "0.1.0"}],\n "jsc-git@jsc": [{"name": "jsc-git", "installPath": "%s/p/git", "version": "0.1.0"}],\n "jsc-pkg@jsc": [{"name": "jsc-pkg", "installPath": "%s/p/pkg", "version": "0.1.0"}]\n}\n' \ + "$vg_home" "$vg_home" "$vg_home" "$vg_home" "$vg_home" > "$vg_reg" + # jsc-cli 自己也宣告一個落後的相依:豁免那一條要證明的是「豁免技能在相依落後的狀態下 + # 照樣放行」,它所屬的 plugin 沒有落後的相依就什麼都證不到。 + printf '{"name": "jsc-cli", "version": "0.1.0", "jsc": {"requires": {"jsc-hooks": ">=9.9.9"}}}\n' \ + > "$vg_home/p/cli/plugin.json" + printf '{"name": "jsc-hooks", "version": "0.1.0"}\n' > "$vg_home/p/hooks/plugin.json" + # 沒有 jsc.requires 的 manifest,留給 fail-open 那一條用。 + printf '{"name": "jsc-git", "version": "0.1.0"}\n' > "$vg_home/p/git/plugin.json" + # p/pkg 刻意只有目錄、沒有 plugin.json,那是「讀不到 manifest」那一條的材料。 + + # 一、相依確定落後:擋下,而且訊息要指名是哪一項、需要哪一版、目前哪一版。 + printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-cli": ">=9.9.9"}}}\n' \ + > "$vg_manifest" + smoke_vg_case "相依落後" jsc-sdlc:implement 2 + smoke_vg_msg "相依落後時訊息指名那一項" "jsc-cli 需要 >=9.9.9,目前 0.1.0" + + # 二、相依相等或超前都放行:只擋落後,本機超前是開發技能組時的常態,擋了維護者自己動不了。 + printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-cli": ">=0.1.0"}}}\n' \ + > "$vg_manifest" + smoke_vg_case "相依版本相等" jsc-sdlc:implement 0 + printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-cli": ">=0.0.9"}}}\n' \ + > "$vg_manifest" + smoke_vg_case "相依版本超前" jsc-sdlc:implement 0 + + # 三、豁免技能:這一條最要緊,它證明閘門不會把更新與修復的唯一路徑鎖死。同一個 jsc-cli + # plugin 底下,非豁免的技能在相依落後時被擋、豁免的那一支照樣放行,差別只在豁免清單。 + smoke_vg_case "豁免技能 jsc-cli:deploy,相依落後照樣放行" jsc-cli:deploy 0 + smoke_vg_case "同 plugin 的非豁免技能 jsc-cli:doctor" jsc-cli:doctor 2 + + # 四、fail-open 四條:沒有證據不等於落後。五支 CLI 只有 claude 讀得到本機載入版本, + # 這四條任何一條改成擋人,另外四支就會被整批鎖死。 + smoke_vg_case "fail-open:解不出安裝路徑" jsc-log:worklog 0 + smoke_vg_case "fail-open:讀不到 manifest" jsc-pkg:pkg-update 0 + smoke_vg_case "fail-open:manifest 沒有 jsc.requires" jsc-git:commit 0 + printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-review": ">=9.9.9"}}}\n' \ + > "$vg_manifest" + smoke_vg_case "fail-open:讀不到相依 plugin 的本機載入版本" jsc-sdlc:implement 0 + + # 五、逃生門蓋過相依落後:離線工作時要留一條自己解得開的路。 + printf '{"name": "jsc-sdlc", "version": "0.1.0", "jsc": {"requires": {"jsc-cli": ">=9.9.9"}}}\n' \ + > "$vg_manifest" + smoke_vg_case "逃生門 JSC_VERSION_GUARD=off 蓋過相依落後" jsc-sdlc:implement 0 JSC_VERSION_GUARD=off + + # 回歸:真實 manifest 是多行縮排的,jsc.requires 的最後一個鍵一樣要解得到。解析先把整份 + # JSON 併成一行,那條串流結尾沒有換行時,read 會把最後一筆的值讀進去卻回非零,while 迴圈 + # 的本體一次都跑不到,落後的相依就靜靜被漏掉。這裡把落後的那一項刻意擺在最後一個鍵, + # 前面那一項則是滿足的:漏掉最後一筆就會變成放行,當場被這一條抓出來。 + printf '{\n "name": "jsc-sdlc",\n "version": "0.1.0",\n "jsc": {\n "requires": {\n "jsc-hooks": ">=0.0.1",\n "jsc-cli": ">=9.9.9"\n }\n }\n}\n' \ + > "$vg_manifest" + smoke_vg_case "多行縮排 manifest,落後的相依擺在最後一個鍵" jsc-sdlc:implement 2 + smoke_vg_msg "最後一個鍵的落後項也列進訊息" "jsc-cli 需要 >=9.9.9,目前 0.1.0" + rm -rf "$vg_home" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] version-guard.sh:建不出暫存目錄,相依版本判定沒驗到。\n' >> "$smoke_out" + fi + # 自我斷言:實際跑過的條數對上宣告的預期條數,再對上真正印出來的行數。三邊一致才算數, # 少跑一條或多印一行都會在這裡現形,散文就不必再自己記一份數字。 smoke_lines=$(wc -l < "$smoke_out" 2>/dev/null | tr -d ' ') [ -n "$smoke_lines" ] || smoke_lines=0 smoke_total=$((SMOKE_EXPECT_HOOK + SMOKE_EXPECT_MODEL + SMOKE_EXPECT_WP \ - + SMOKE_EXPECT_RS + SMOKE_EXPECT_WG)) + + SMOKE_EXPECT_RS + SMOKE_EXPECT_WG + SMOKE_EXPECT_VG)) smoke_mismatch="" [ "$smoke_n_hook" = "$SMOKE_EXPECT_HOOK" ] \ || smoke_mismatch="${smoke_mismatch}hook 模式 $smoke_n_hook 條(預期 $SMOKE_EXPECT_HOOK);" @@ -1051,10 +1160,12 @@ if [ "$action" = smoke ]; then || smoke_mismatch="${smoke_mismatch}重啟閘門 $smoke_n_rs 條(預期 $SMOKE_EXPECT_RS);" [ "$smoke_n_wg" = "$SMOKE_EXPECT_WG" ] \ || smoke_mismatch="${smoke_mismatch}寫入閘門 $smoke_n_wg 條(預期 $SMOKE_EXPECT_WG);" + [ "$smoke_n_vg" = "$SMOKE_EXPECT_VG" ] \ + || smoke_mismatch="${smoke_mismatch}相依版本 $smoke_n_vg 條(預期 $SMOKE_EXPECT_VG);" [ "$smoke_lines" = "$smoke_total" ] \ || smoke_mismatch="${smoke_mismatch}結果行數 $smoke_lines 行(預期 $smoke_total);" - smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、模型來源 $SMOKE_EXPECT_MODEL 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條" + smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、模型來源 $SMOKE_EXPECT_MODEL 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條、相依版本 $SMOKE_EXPECT_VG 條" if [ -n "$smoke_mismatch" ]; then printf 'status=failed reason=%s\n' "冒煙結果行數與預期不符:${smoke_mismatch}判定路徑有增減時要一併改腳本裡的預期條數" printf 'lines\t%s\n' "$smoke_lines" @@ -1065,7 +1176,7 @@ if [ "$action" = smoke ]; then fi if [ "$smoke_fails" -eq 0 ]; then - printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型來源、工作包歸屬、部署後重啟閘門與寫入提交閘門的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤" + printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,模型來源、工作包歸屬、部署後重啟閘門、寫入提交閘門與相依版本檢查的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤" printf 'lines\t%s\n' "$smoke_lines" cat "$smoke_out"; rm -f "$smoke_out"; exit 0 fi