問題
六個前端 repo,每一家都有 lint、型別檢查、覆蓋率門檻、e2e、依賴弱點掃描。全部設定齊全,全部看起來在守。實際從頭跑過一次之後,每一道都有至少一處是空轉的。
不是設定寫錯,是設定寫的位置,跟實際執行的路徑不一樣。設定檔讀起來完全正確,所以 review 看不出來;守門從來沒紅過,所以也沒有人去懷疑。
六個實例
| 你以為 | 實際上 | 怎麼發現的 |
|---|---|---|
karma.conf.js 的 90/80 門檻在守 | angular.json 沒有 karmaConfig 欄位,Angular CLI 用的是內建設定,整份 karma.conf 被忽略 | 追 CLI 原始碼,看到內建設定只在沒指定 karmaConfig 時使用 |
| 覆蓋率守門讀報告比對門檻 | 測試失敗時不產生新報告,守門讀上一次的舊報告並印「通過」 | ng test 退出碼 1、報告完全沒產生,同一次執行卻印出 92.99% 通過 |
make check 會跑覆蓋率 | 有一家的 check 少了產生報告那一步,直接讀舊的 | 加上報告時效檢查後才擋出來 |
pnpm overrides 壓版本修弱點 | 全域壓,把只吃舊版 API 的消費者打壞——六家容器內的 eslint 全都是壞的 | 第一次真的跑 make check |
| 五家都有 e2e 測試 | 沒接進任何守門,從沒跑過;瀏覽器只裝在 host | 接起來跑第一次就抓到三個 bug |
git branch -r 顯示遠端有這些分支 | 那是本機快取,19 條裡有 12 條在伺服器上早就不存在 | 實際刪除時回報 remote ref does not exist |
第一列的背景:Angular 官方文件說 CLI 會依 angular.json 的選項「在記憶體裡組出完整的 Karma 設定」,要自訂才需要用 ng generate config karma 產出 karma.conf.js(Angular: Testing with Karma)。也就是說,檔案擺在那裡不代表它被讀到。補上 karmaConfig 之後,json-summary 報告才產得出來——修之前覆蓋率目錄裡只有 html。
e2e 第一次真的跑起來抓到的三個問題,都是從來沒人發現過的:一個 Astro + Vue 的導覽列在 webkit 有 hydration mismatch;一份 Playwright 設定的 baseURL 指向 5173,但 webServer 起在 4173;一組斷言還停在舊版的頁面文案。
判準:不要問「設定寫了嗎」,要問「怎麼證明它在擋」
四種可以立刻執行的證明方式:
一、把守門弄紅一次。 這是最強的證據。加一條會失敗的斷言、把某個實作改成沒作用、把門檻調到現況之上,看它會不會擋。守門從來沒紅過,就等於從來沒被驗證過。
實例:把關閉刪除對話框的函式改成什麼都不做,確認新寫的斷言真的會紅;把覆蓋率門檻臨時調到 90,確認覆蓋率檢查腳本會 exit 1。
二、從乾淨環境重建一次。 容器的 node_modules 掛的是匿名 volume。每次 docker compose run 都會建一個新容器、配一個新的匿名 volume,再從 image 把內容複製進去(Docker: Volumes)。所以在容器內 pnpm install 只活在那一個容器裡,要重建 image 才算數。舊 image 會遮住依賴層面的所有問題,也會讓覆蓋率量出來的數字失真:其中一家從「66%」變成真實的 56%。
三、問權威來源,不要問快取。 git branch -r 列的是本機的 remote-tracking 參照,伺服器上刪掉的分支要等 git fetch --prune 才會清掉(git-fetch);git ls-remote 問的才是伺服器的真實狀態。同理,coverage-summary.json 存在,不代表它是這次跑出來的,要看 mtime。
四、跑一次完整的 make check,而不是只跑 pre-commit。 這兩者走的路徑不同:pre-commit 的 lint-staged 跑在 host,make check 的 lint 跑在容器。六家容器內的 eslint 壞了很久,就是因為只有 make check 會碰到它,而 make check 從沒被跑過。
overrides 的正確用法
弱點修補是這一輪最大的單一問題來源。判準很簡單:
advisory 影響哪條版本線,override 就只釘哪條線。
- 先看 advisory 的影響版本與修補版本。這次的 ajv 那條(GHSA-2g4f-4pwh-qvx6),影響範圍是
>= 7.0.0-alpha.0, < 8.18.0與< 6.14.0,修補版分別是 8.18.0 與 6.14.0(GitHub Advisory)。ajv 6 的修補線就在 6.x 裡,全域把所有人壓成 8 純粹是誤傷——@eslint/eslintrc與@angular-eslint需要 ajv 6,容器內的 lint 因此全部失敗在NOT SUPPORTED: option missingRefs。 - pnpm 的
parent>child語法可以把 override 限定在某個父套件底下(pnpm: overrides):"@angular-devkit/core>ajv": "^8.18.0"、"eslint-plugin-import>minimatch": "^3.1.2"。("pkg@version"那種分線寫法先前試過不生效。) - 該刪還是該限定範圍,用實測判斷:拿掉 override 之後弱點數變不變。五家拿掉 ajv override 之後,弱點數完全不變,代表它沒在擋任何東西,直接刪。唯一的例外是 Angular 那家:弱點真的來自
@angular-devkit/core帶進來的 ajv 8.17.1,改成限定範圍的寫法之後,ajv 6 與 8 兩條線並存,lint 通過,弱點也回到門檻內。
minimatch 是同一個故事的另一個版本:一家全域把 minimatch 壓到 ^9,但 eslint-plugin-import 和 glob@7 需要 v3 的 CommonJS 預設匯出,容器內 lint 失敗在 (0 , _minimatch2.default) is not a function。改成只對這兩個父套件釘 ^3.1.2 之後,lint 恢復,弱點數也沒有變多。
怎麼預防再次發生
- 新增任何守門設定時,同一個 commit 內附上「它會紅」的證據:臨時破壞一次、貼出紅的輸出、再還原。
- 守門腳本本身要防「讀到舊資料」。覆蓋率檢查現在會拒絕超過 30 分鐘的報告,並在訊息裡說明「測試失敗時不會產生新報告,舊報告會讓守門誤判通過」。
make check必須是 pre-commit 的超集,而且至少在合併前跑一次。pre-commit 為了速度走 host、只看 staged 檔案,天生涵蓋不到容器與全量。- 動
pnpm overrides之後,容器 image 要重建再驗;只更新 lockfile 不算數。
相關
- 驗收假綠形態目錄:「拿舊報告當新結果」與「門本身是 no-op」兩種形態,就是這篇第二、第一個實例的通用版本;那篇的通則「新寫的檢查,第一件事是讓它紅一次」跟這裡的第一種證明方式是同一件事。
- 「兩種情況都寫」的容錯設定會兩頭落空:同樣是覆蓋率守門,那篇的問題是設定兩頭都沒命中、產出空報告靜默通過——也是「設定看起來對、實際沒在擋」。
- 覆蓋率守門的假綠:專講覆蓋率的三種錯覺——分母不完整、門檻被忽略、mock 讓測試驗不到東西。這篇的覆蓋率實例是其中一種。