Skip to content

test: the snapshot-collision guard walked into node_modules and killed the vitest worker #485

Description

@IgorShevchik

Environment

main at 5a06163d. Found while adding the smoke suite in #483.

This issue was filed with the wrong diagnosis and has been rewritten. The original text blamed an unbounded per-worker leak in vitest and called the cause unknown. It is not a vitest defect; it is one line in a spec added by #480. The bisection and the fix are below. The wrong version is kept in the edit history because the way it was wrong is the interesting part — see the last section.

The defect

test/utils/indistinguishable-snapshots.spec.ts (added in #480) collects every .snap file under test/:

readdirSync(root, { recursive: true, encoding: 'utf8' })
  .filter(f => f.endsWith('.snap'))

{ recursive: true } has no ignore option. It walks everything and returns every path in a single array — and test/ contains installed dependency trees: test/nuxt/node_modules, plus test/smoke/fixture/node_modules once #483 adds that workspace package. The array is hundreds of thousands of strings, built to find 212 files.

Measured on a 4-core machine:

wall time result
readdirSync('test', { recursive: true }) > 120 s, killed never returned
pruned walk 2 ms, 46 MB RSS the same 212 files, the same two directories

The worker died at the heap limit. Vitest reported it as:

FATAL ERROR: Ineffective mark-compacts near heap limit
Error: [vitest-pool]: Worker forks emitted error.
Caused by: Error: Worker exited unexpectedly

with no file name, which is what made it look like a suite-wide condition rather than one spec.

Why it looked like it was everywhere

run files result
main @ 6f0e71ef 314 green
#483, first push 315 OOM
#483, after moving the module suite out 314 OOM
--project vue alone, on main 153 152 passed, 1 error
--project vue, --max-old-space-size=1024 153 152 passed, 1 error

The last row is the one that named it. If memory accumulated across files, an eight-times-smaller heap would have died eight times sooner. It died at exactly the same point, so exactly one file was responsible, and --reporter=verbose diffed against the collected list said which.

main was green because the cost scales with what is installed under test/: test/nuxt/node_modules alone stayed (barely) under the runner's 4 GB. #483 added a second dependency tree there and pushed it over — which is why the failure arrived with a pull request that never touched the guard.

Fix

Shipped in #483. snapshotFiles() is a hand-rolled walk that prunes as it descends:

const PRUNED = new Set(['node_modules', '.nuxt', '.output', '.data', '.cache', 'dist'])

Snapshots never live in any of those, so nothing is lost. A regression test builds a temp tree containing node_modules/pkg/__snapshots__/Vendored.spec.ts.snap and .output/server/Built.spec.ts.snap and asserts neither is returned.

What to take from it

The guard was reviewed six times in #480 — by /review and five reviewers — and every one of them, including the two who ran it, saw { recursive: true } and read it as "walks the tree". It does. The cost of walking this tree was invisible because nothing about the code says how big the tree is, and the spec passed everywhere it was run.

Two things would have caught it earlier and neither existed:

  • the failure names nothing. Worker exited unexpectedly with no file is the whole reason this took an afternoon. Vitest cannot do better on a SIGKILL-class death, but a CI step that ran each project separately would at least have narrowed it to vue in one run.
  • the smaller-cap bisect is cheap and was not tried first. NODE_OPTIONS=--max-old-space-size=1024 turns "eight minutes, then a mystery" into "the same failure, four times faster, and now it is one file". Worth writing into the testing notes as the first move on any OOM here.

The second is a real gap in .github/contributing/testing.md and is worth a follow-up on its own.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtesting

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions