「PR 合併了、issue 也關閉了,這件事應該就結束了吧?」
前面 23 天分別拆開講過 issue triage、PR review、bug 定位這些單一環節,但真實世界的修復過程很少乾淨俐落地走一輪就結束。今天用一個完整的真實案例,從頭到尾走一次——你會看到,「合併」不是故事的終點,第一版修復本身也可能藏著新的問題。
${workspaceFolder} 沒有被正確解析這個真實案例的起點是 issue #410:一位使用者的 PHP 專案放在子目錄(例如 apps/api),並在 VS Code 設定裡用 ${workspaceFolder} 這個變數組出 PHPUnit 設定檔的路徑。執行單一測試時一切正常,但在「探索測試」(test discovery,也就是套件掃描專案、把測試顯示在 Test Explorer 面板裡的階段)時,${workspaceFolder} 卻沒有被替換成實際路徑,導致套件讀不到正確的設定檔。
當天就有一位外部貢獻者送出 PR #412,PR 描述直接寫著「Fixes #410」,並說明根因:測試探索階段讀取 PHPUnit 參數的時機,是在替換工作區變數「之前」,所以 --configuration 參數裡的 ${workspaceFolder} 從來沒被解析過,導致設定檔路徑對不上。這個 PR 補上了在設定檔解析階段做變數替換的邏輯,還加了一支回歸測試,同一天就被合併。
看起來故事到這裡就該結束了——症狀消失、有回歸測試、issue 也被自動關閉。但這正是今天要講的重點:一個修復「解決了回報的症狀」,不等於「修復得完全正確」。
沒隔多久,另一位外部貢獻者在 PR #413 裡提出了一個更細緻的觀察:用類似的設定(phpunit.phpunit 跟 phpunit.args 都帶 ${workspaceFolder}),「探索測試」階段會把 ${workspaceFolder} 解析成專案根目錄,但「實際執行測試」階段卻解析成 apps/api 子目錄——同一個變數,在同一次操作的兩個不同階段,被解析成兩個不同的值。這位貢獻者甚至在 PR 描述裡直接標記維護者,問了一句「這樣對嗎?」,而不是自己斷定這是不是 bug。
這個發現本身就是一個很好的示範:PR #412 的回歸測試綠燈,不代表這個功能在所有使用情境下都一致——它只證明了「原本回報的那個具體症狀」被修好了,沒有證明「這個變數解析邏輯在探索跟執行兩個階段是不是同一套規則」。
緊接著的 PR #414,由維護者本人提出,PR 描述寫得很直白:PR #413 裡新增的一支測試,斷言了一個「雙重巢狀路徑」(/workspace/apps/api/apps/api/...)是預期行為,但那其實是在示範一個 bug,不是在驗證正確行為。這支 PR 把那個誤導性的斷言,換成一個語意正確的檢查:驗證當 workspaceFolder 沒有被提供時,會正確退回使用 cwd。
走到這一步,這個 issue 真正的生命週期才算完整:回報 → 第一版修復(解決症狀)→ 另一位貢獻者發現行為不一致(延伸出新問題)→ 維護者修正第一版修復裡「連測試本身都寫錯」的地方。四個步驟,橫跨兩個不同的外部貢獻者跟維護者本人,中間沒有一步是憑空跳過的。
用一組對照來看今天的重點:
❌ 追蹤 issue 只看到「合併就結案」:
「PR #412 合併了、issue #410 關閉了,這個問題處理完了。」
→ 沒有繼續追蹤這個修復本身有沒有引入新的不一致,
也沒有意識到「回歸測試綠燈」只證明了原本症狀消失
✅ 追蹤 issue 到「修復本身也被驗證過」:
「PR #412 修好了原本回報的症狀,
但 PR #413 發現探索/執行兩階段解析不一致,
PR #414 進一步發現 #413 的測試斷言本身也寫錯了——
這三個 PR 合起來,才是這個 issue 真正被妥善處理的完整過程。」
→ 每一版修復都可能是下一輪檢查的起點,而不是終點
如果讓 AI 幫忙追蹤這類 issue,它能做的是把這四個步驟的因果關係整理清楚——哪個 PR 解決了什麼、留下了什麼、後續哪個 PR 又修正了什麼;但「這個修復到底有沒有真的修對」這個判斷,仍然需要人(或者另一位獨立的貢獻者)帶著懷疑重新檢視一次,就像 PR #413 的作者那樣,不假設前一個修復是對的,而是先問一句「這樣對嗎?」
回想你追蹤過的某個 issue:它是「PR 合併就結案」,還是你曾經回頭檢查過那個修復本身有沒有留下新的問題?如果從沒回頭檢查過,你怎麼確定那次修復是完整的?
明天要看一個方向相反的案例:一次 AI 漏看了一個 edge case,直到使用者在 issue 討論裡親自糾正,才發現遺漏——這跟今天「連續多輪檢查才收斂到正確」的故事,是同一個道理的另一種呈現方式。