test(smoke): 接線腳本冒煙補上相依版本檢查的十三條判定斷言

接線腳本的 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) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 13:41:25 +08:00
co-authored by Claude Opus 5
parent 5c2727bf56
commit 15f7dcc5fb
+115 -4
View File
@@ -22,6 +22,9 @@
# 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。 # 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。
# write-guard.sh 的四種模式同法:階段鎖、稽核技能、提交指令與 release 解除各自的判定路徑, # write-guard.sh 的四種模式同法:階段鎖、稽核技能、提交指令與 release 解除各自的判定路徑,
# 都用暫時的 $JSC_HOME 跑過一次並比對結束碼。 # 都用暫時的 $JSC_HOME 跑過一次並比對結束碼。
# version-guard.sh 的相依版本檢查同法,只是沙箱換成暫時的 HOME(註冊檔路徑由 $HOME 決定):
# 相依落後的擋人與訊息內容、相等與超前的放行、豁免技能在相依落後時照樣放行、四種 fail-open、
# 逃生門,再加一條回歸——多行縮排的 manifest,jsc.requires 的最後一個鍵也要解得到。
# #
# smoke 自己數結果行並自我斷言:`lines<TAB>{數量}` 那一行印的是其後 `[jsc]` 結果行的實際條數, # smoke 自己數結果行並自我斷言:`lines<TAB>{數量}` 那一行印的是其後 `[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、 # hook 模式 九支 hook 的每個接線模式各一條。sdlc-gate.sh、comment-scope.sh、
@@ -752,11 +755,13 @@ if [ "$action" = smoke ]; then
# 工作包 sdlc-gate.sh wp-check skill 的歸屬判定路徑 # 工作包 sdlc-gate.sh wp-check skill 的歸屬判定路徑
# 重啟閘門 restart-gate.sh 的判定、清除路徑與狀態檔範圍檢查 # 重啟閘門 restart-gate.sh 的判定、清除路徑與狀態檔範圍檢查
# 寫入閘門 write-guard.sh 三種擋人模式與 release 解除模式的判定路徑 # 寫入閘門 write-guard.sh 三種擋人模式與 release 解除模式的判定路徑
# 相依版本 version-guard.sh 讀 manifest jsc.requires 的擋人、放行、豁免與 fail-open 路徑
SMOKE_EXPECT_HOOK=17 SMOKE_EXPECT_HOOK=17
SMOKE_EXPECT_MODEL=4 SMOKE_EXPECT_MODEL=4
SMOKE_EXPECT_WP=6 SMOKE_EXPECT_WP=6
SMOKE_EXPECT_RS=16 SMOKE_EXPECT_RS=16
SMOKE_EXPECT_WG=21 SMOKE_EXPECT_WG=21
SMOKE_EXPECT_VG=13
# 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略) # 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略)
# $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。 # $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。
@@ -1034,12 +1039,116 @@ if [ "$action" = smoke ]; then
printf '[jsc] write-guard.sh:建不出暫存目錄,寫入與提交閘門判定沒驗到。\n' >> "$smoke_out" printf '[jsc] write-guard.sh:建不出暫存目錄,寫入與提交閘門判定沒驗到。\n' >> "$smoke_out"
fi fi
# 相依版本檢查(version-guard.sh 的第二種擋人情況):上面那支只驗「hook 跑得完」,
# manifest 的 jsc.requires 一條判定路徑都沒走到。這裡自備一份暫時的 HOME,把假的
# installed_plugins.json 與各 plugin 的 manifest 都放進去,逐條跑判定並比對結束碼。
# 覆寫 HOME 是必要的:註冊檔路徑由 $HOME 決定,不覆寫就會讀到使用者真正的安裝清單,
# 冒煙結果變成看那台機器裝了哪幾版的臉色,也會在別人的安裝目錄上留下痕跡。
# GITEA_HOST 一律清空:放行的案例會繼續往下走到遠端比對,站台推導得出來就會真的連網,
# 冒煙不該依賴網路,也不該讓遠端落後把預期放行的案例判成擋下。
# 一律 </dev/null:hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
smoke_vg_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=額外環境設定(KEY=值,可省略)
# 第四個參數省略時仍要餵一個合法的 KEY=值 給 env,否則它會把空字串當成要執行的指令。
# 這個名字沒有任何 hook 讀它,只是佔位。
_extra="${4:-JSC_VERSION_GUARD_UNUSED=1}"
# 輸出留在變數裡給下面的訊息斷言比對:擋人訊息與結束碼是同一次執行的兩件事,
# 為了比訊息再跑一次,比到的就可能不是同一次的結果。
vg_out=$(env HOME="$vg_home" JSC_HOME="$vg_home/.jsc" GITEA_HOST="" JSC_CLI="$cli" \
JSC_TOOL_NAME=Skill JSC_SKILL="$2" SKILL="$2" "$_extra" \
sh "$HOOKS/version-guard.sh" </dev/null 2>&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 ' ') smoke_lines=$(wc -l < "$smoke_out" 2>/dev/null | tr -d ' ')
[ -n "$smoke_lines" ] || smoke_lines=0 [ -n "$smoke_lines" ] || smoke_lines=0
smoke_total=$((SMOKE_EXPECT_HOOK + SMOKE_EXPECT_MODEL + SMOKE_EXPECT_WP \ 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_mismatch=""
[ "$smoke_n_hook" = "$SMOKE_EXPECT_HOOK" ] \ [ "$smoke_n_hook" = "$SMOKE_EXPECT_HOOK" ] \
|| smoke_mismatch="${smoke_mismatch}hook 模式 $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_mismatch="${smoke_mismatch}重啟閘門 $smoke_n_rs 條(預期 $SMOKE_EXPECT_RS);"
[ "$smoke_n_wg" = "$SMOKE_EXPECT_WG" ] \ [ "$smoke_n_wg" = "$SMOKE_EXPECT_WG" ] \
|| smoke_mismatch="${smoke_mismatch}寫入閘門 $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_lines" = "$smoke_total" ] \
|| smoke_mismatch="${smoke_mismatch}結果行數 $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 if [ -n "$smoke_mismatch" ]; then
printf 'status=failed reason=%s\n' "冒煙結果行數與預期不符:${smoke_mismatch}判定路徑有增減時要一併改腳本裡的預期條數" printf 'status=failed reason=%s\n' "冒煙結果行數與預期不符:${smoke_mismatch}判定路徑有增減時要一併改腳本裡的預期條數"
printf 'lines\t%s\n' "$smoke_lines" printf 'lines\t%s\n' "$smoke_lines"
@@ -1065,7 +1176,7 @@ if [ "$action" = smoke ]; then
fi fi
if [ "$smoke_fails" -eq 0 ]; then 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" printf 'lines\t%s\n' "$smoke_lines"
cat "$smoke_out"; rm -f "$smoke_out"; exit 0 cat "$smoke_out"; rm -f "$smoke_out"; exit 0
fi fi