Skip to content

Unpin vitest@^4 once stryker-js#6210 (Vitest 5 false survivors) is fixed #35

Description

@ddteeter

Summary

vitest@5 breaks StrykerJS's vitest runner: every covered mutant is reported Survived, because each mutant run executes zero tests. We are pinned to vitest@^4 until upstream ships a fix.

Upstream: stryker-mutator/stryker-js#6210"vitest-runner: on Vitest 5 the per-test name filter matches nothing, so every covered mutant survives (testNamePattern now joins the chain with ' > ')", filed 2026-09-04, open.

Vitest 5 changed the testNamePattern separator to ' > '; the runner still builds the filter with a space join, so the filter selects nothing.

Measured

Bare greenfield repo, one tier(spend) function, tests asserting every boundary. Same code, same tests, same guardrails — only the stack differs:

stack result
stryker 10 + vitest-runner 10 + vitest 4 12 Killed ✅
stryker 10 + vitest-runner 10 + vitest 5 12 Survived
stryker 9 + vitest-runner 9 + vitest 4 12 Killed ✅

So Stryker 10 is fine; Vitest 5 is the breaking variable.

A probe test appended to a log file on every execution ran once (the dry run) and never again, with coverageAnalysis: "off" and 13 mutants — the mutant runs execute no tests at all.

No configuration works around it. All six of coverageAnalysis ∈ {all, perTest, off} × vitest.related ∈ {default, false} report 12/12 Survived.

Why this matters here

Mutation testing is the highest-value check this pack runs on agent-written code, and this failure mode is silent and inverted: it manufactures violations rather than suppressing them. In a fully-agent-driven repo the Stop gate hands the fixer unkillable work, the escalation ladder exhausts, and the two exits the agent can actually reach — a sanctioned suppression, or analyzers.stryker: "off" — are both forbidden by the scaffolded AGENTS.md. The guardrail ends up teaching the agent to disable the guardrail.

What we shipped instead

We cannot fix the runner, but we can stop it from lying. The broken run has an exactly discriminating signature in the report:

coveredBy testsCompleted
genuine survivor (vitest 4, vacuous test) non-empty 1 — all 9
broken runner (vitest 5) non-empty 0 — all 12

A mutant with non-empty coveredBy and testsCompleted === 0 is not evidence of survival; it is a run that never happened. runStryker raises one guardrails/analyzer-failed for that instead of a storm of false stryker/survived.

This is version-independent and covers the rest of that runner's open false-survivor family — upstream #6146 (the same invariant, proposed upstream, unmerged since July), #6150, #6179, #6209.

Revisit when

  • stryker-js#6210 closes and a @stryker-mutator/vitest-runner release ships the fix
  • Verify against the fixture: raise vitest to ^5 and confirm the drift guard still reports Killed
  • Unpin vitest in package.json and in the adopting-guardrails worked-example table
  • Re-check whether the zero-test-run guard should stay (it should — it is a general invariant, not a #6210 workaround)

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 workingdependenciesPull requests that update a dependency file

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions