v8 coverage 在循環引用與深層元件樹下會死鎖
症狀
MR 828 的 test:unit job 連續兩次跑滿 1 小時被 GitLab runner kill,failure_reason: job_execution_timeout。trace 沒有任何 fail,只是某個檔案進去就再也沒出來。
本機 yarn vitest run <file> 8 秒過,但 yarn vitest run --coverage <file> 卡 4 分鐘。CI script 是 yarn test → vitest run --coverage,差別就在 --coverage。
Root Cause
@vitest/coverage-v8 在 instrument 整張 module graph 時死鎖。
毒在 graph 太深:
- 測試 render 的是真實 page,page 拉真實 child(
UncertaintyAssessmentTable/UncertaintyAssessmentItemDrawer) - child 拉 Chakra Drawer / Portal /
TruncatedHoverText(含 ResizeObserver + rAF)/HighlightMatchedText - 還疊上一條循環引用:
page → @/utils/route → @/RouteConfig → page
v8 一邊 instrument 一邊解析這張 graph + circular,worker 進去就出不來。沒 --coverage 不會死,因為 v8 不需要 instrument。
解法
Mock 掉真實 child 元件,把 graph 縮到只剩 page 自己。Production code 0 行改動。child mock 要對齊 page 對 props 的契約(form、按鈕、cancel → useModal、edit/delete、disable 條件)。
關鍵 commit 42bdb1c9。
教訓
- CI timeout 不一定是測試慢,可能是 coverage instrumentation 死鎖。 本機跑
--coverage是必要的 reproduce 步驟,光跑vitest run看不出來。 - Page-level test 用真實 child 在 v8 coverage 下是地雷。 一旦 child 拉到 Chakra Portal、ResizeObserver、rAF 這類東西,加上任何循環引用就有風險。Page 測試該 mock child,保留覆蓋率給 child 自己的 test。
- Timeout 會吃掉真實 assertion failure。 修死鎖後才看到一個 stale
descriptionassertion,本來 CI 也會 fail,只是被 v8 timeout 蓋掉。 - 不要動 production code 解循環引用 是用戶當下的選擇 — mock 是更小的 blast radius;真要根治 circular 是另一個 PR 的事。
比較
| 維度 | 改前 | 改後 |
|---|---|---|
| v8 要 instrument 的 graph | page + Drawer + Portal + Chakra 一大坨 + circular | page 自己(child 是純函式 stub) |
| circular import 影響 | RouteConfig 把整個 page enum 拉進來 | mock factory 直接擋掉 child import 鏈 |
| 測試契約 | 真實 Drawer/Table | Mock 對齊 props 契約 |