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
Summary
vitest@5breaks StrykerJS's vitest runner: every covered mutant is reportedSurvived, because each mutant run executes zero tests. We are pinned tovitest@^4until 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 (
testNamePatternnow joins the chain with' > ')", filed 2026-09-04, open.Vitest 5 changed the
testNamePatternseparator 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: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 scaffoldedAGENTS.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:
coveredBytestsCompleted1— all 90— all 12A mutant with non-empty
coveredByandtestsCompleted === 0is not evidence of survival; it is a run that never happened.runStrykerraises oneguardrails/analyzer-failedfor that instead of a storm of falsestryker/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-mutator/vitest-runnerrelease ships the fixvitestto^5and confirm the drift guard still reports Killedvitestinpackage.jsonand in theadopting-guardrailsworked-example table