昨天介紹完 vscode-phpunit 這個專案,我猜有讀者心裡冒出這個疑問:一個讓 PHPUnit/Pest 測試在 VS Code 裡跑起來的擴充套件,功能聽起來單純——找到測試、執行、把結果畫回 Test Explorer,這能有多複雜?
答案是:複雜的從來不是「功能」本身,而是這個功能要在多少種環境組合下都成立。同一段「執行測試、回報結果」的邏輯,遇到本機執行是一種寫法,遇到 Docker 容器是另一種路徑映射問題,遇到 SSH 遠端主機又是一種,遇到 Laravel Sail 包一層 sail artisan test 又是一種。使用者的專案結構、PHP 版本、PHPUnit 版本、Pest 版本、VS Code 版本、作業系統,任何一個維度變了,原本沒事的邏輯都可能壞掉。
今天要具體拆這個「相容性矩陣」對維護者意味著什麼,以及維護者每天實際在處理的工作,哪些看起來瑣碎、哪些其實需要判斷力。
vscode-phpunit 要同時處理的變數,粗略列出來就有這幾層:
docker compose exec)、SSH 遠端、Laravel Sail(sail artisan test 包了一層 Sail 自己的路徑映射)--parallel
單獨看每一個維度都不難處理,難的是這些維度會兩兩甚至三三組合出現。「Pest + Docker + --parallel」是一種組合、「PHPUnit + SSH + Xdebug」是另一種組合、「Pest v4 + Laravel Sail + dataset」又是一種。維護者沒辦法窮舉所有組合去手動測,大部分時候是等使用者在某個特定組合下踩到 bug,才知道這個交集原來有問題。
這不是空談,這個專案現在(2026-08-30 這幾天)掛著的 open issue 剛好可以拿來當活教材。
先看 #416「Failed to run docker compose command」。回報者的執行環境是 Docker + frankenphp + Symfony,phpunit.command 設定裡用 docker compose -f docker-compose-test.yml up -d php-e2e 先把容器啟起來,再用 exec 執行測試。結果套件在組指令時把 -f 這個 flag 位置解析錯了,噴出 unknown shorthand flag: 'f' in -f。
再看 #417「Impossible to run individual test」。同一位回報者、同一組 Docker 設定,這次踩到的是另一個問題:套件在 Docker 環境下「執行單一測試」(右鍵 Run test)時,沒有把測試檔案的路徑做容器內外的映射轉換,導致送進容器的是宿主機的絕對路徑,容器裡當然找不到這個檔案。回報者甚至在 #417 的內容裡直接留言註記「這裡先繞開,等 #416 修好」——兩個 issue 是同一個人、同一個 Docker 專案結構,卡在同一層「路徑到底該用宿主機視角還是容器視角」的問題上,只是分別在「啟動指令」跟「單一測試路徑映射」這兩個不同的程式碼路徑上各爆一次。
再看 #415「Not evaluating test results when running pest in parallel」。這是 Pest v4 + Laravel Sail + --parallel 的組合:使用者一加上 --parallel 這個參數,測試結果就全部被標成 skipped,而拿掉 --parallel 就一切正常。有意思的是,回報者自己在 issue 內文就先做了功課,寫了「這個看起來跟 #402、#408 有關」——這兩個是更早之前、關於 Pest dataset 測試結果解析的 issue。換句話說,「Pest 特有的輸出格式在某些執行模式下解析不完整」這個問題根源,已經不是第一次冒出來,只是這次換了一個新的觸發組合(--parallel)。
這三個 issue 放在一起看,會發現一個維護者每天真實面對的模式:bug 很少是全新的邏輯錯誤,更常是「某個既有邏輯,換一種環境組合排列,原本沒被考慮到的分支被踩到了」。這正是相容性矩陣龐大的專案特有的負擔——不是修一次就永遠修好,而是同一類根因會用不同的排列組合反覆回來敲門。
把最近的 issue 跟 PR 攤開來看,維護者的日常大致落在四類工作,但每一類需要的判斷力差很多。
第一類,bug 回報。像上面 #416、#417、#415 這樣,使用者附上環境設定、錯誤訊息、重現步驟。維護者要做的第一步往往不是馬上動手修,而是先判斷這是不是已知問題的新變種(像 #415 那樣要意識到跟 #402、#408 是同一個根因家族),還是真的獨立的新 bug。這一步判斷做錯,後面的修法方向就會偏。
第二類,功能請求。例如 PR #428「Pest todo() assignee/issue and skipOnCi()/skipLocally() detection」,這是把 Pest 較新版本才有的測試狀態語法(todo() 附加 assignee/issue、條件式跳過)接進套件既有的狀態判讀邏輯。這類工作的判斷成本在於:要跟著上游框架(Pest)的版本演進調整,但不能為了追新語法而破壞舊版本的相容性——這個套件同時要服務還在用 Pest v3 的使用者跟已經升到 v4 的使用者。
第三類,PR 審查。這個專案有不少外部貢獻者送 PR,例如 #413、#412、#411 都是圍繞 workspaceFolder 路徑解析一致性的修正,由外部貢獻者陸續送出(其中兩份出自同一位貢獻者,另一份來自另一位)。審查這類 PR 要判斷的不只是「這段程式碼有沒有語法錯誤」,而是這個修法會不會在某個貢獻者沒測過的環境組合下引入新的回歸——正是因為前面提到的相容性矩陣太大,外部貢獻者往往只驗證了自己遇到問題的那一種組合,維護者要腦補「這個改動會不會影響到 Docker?SSH?Sail?」。
第四類,CI 維護。例如 PR #424「fold e2e tests into the tests.yml matrix instead of a separate workflow」跟 #423「use short --user-data-dir to avoid macOS AF_UNIX socket path limit in e2e」。後者是一個很典型的「CI 環境雜訊」案例——e2e 測試在 macOS runner 上失敗,根因不是程式邏輯錯,是 macOS 對 Unix domain socket 路徑長度有限制,VS Code 測試用的臨時 --user-data-dir 路徑太長就會爆掉。這類問題的判斷成本在於要先分辨「這是我的程式碼壞了」還是「這是這台 CI runner 本身的環境限制」,方向抓錯,會浪費大量時間在錯的地方找 bug。
維護者角色:把 #416、#417、#415 三個 issue 都丟給 AI,
下一句指令是「幫我全部修好」。
AI 的處理方式:
- 各自獨立修,沒有意識到 #416 跟 #417
是同一組 Docker 路徑問題的兩個切面
- 為了讓 #415 的測試通過,加了一個
只在「有 --parallel」時生效的特殊分支,
沒有回頭檢查 #402、#408 的根因是否被一併處理
- 結果:三個 issue 表面上都關閉了,
但 Docker 路徑映射的邏輯变成兩套互不相干的實作,
Pest 平行執行的解析邏輯多了一層局部補丁,
技術債比修之前更重
維護者角色:先讓 AI 針對這批 open issue 做關聯分析——
「找出這幾個 issue 裡,哪些回報者/環境設定/
程式碼路徑有重疊,列出可能同根因的分組」。
AI 產出:
- #416、#417 同一位回報者、同一份 Docker 設定,
都卡在「路徑該用宿主機視角還是容器視角」,
建議合併看,先修路徑映射這一層共用邏輯
- #415 內文已經自己連結 #402、#408,
建議一併回顧這三個 issue 涉及的
Pest 輸出格式解析程式碼是否同一段
維護者角色:確認 AI 的分組合理後,
自己決定修法要往「補一個路徑轉換函式」
還是「重構整個路徑映射層」的方向走——
這個技術方向的判斷,AI 不做決定,人來拍板。
AI 適合做的是「把散落的線索攤開、標出可能的關聯」,真正決定要往哪個方向修、值不值得為此重構一整層邏輯,還是人的工作。 這也是為什麼「相容性矩陣很大」對維護者是負擔而不只是工作量——負擔的核心不是「事情很多」,是「每一件事都可能跟另一件事共享一個看不見的根因,漏看這層關聯,修了等於白修」。
如果你維護過任何一個要同時支援多種執行環境的專案(不限 VS Code 擴充套件,CLI 工具、SDK、CI plugin 都算),回想一下:你手上是不是也有幾個「表面上是不同 bug,其實同一個根因換了個環境組合又冒出來」的案例?你是怎麼發現這層關聯的——是自己看出來的,還是像 #415 那樣,靠回報者自己先做了功課?
明天要具體講 Issue 分類這件事——怎麼讓 AI 先做第一輪 triage,把回報分類、標記優先序、抓出可能重複的回報,人只需要在 AI 的判斷之上做最終確認。
iThome鐵人賽