Appearance
qa-mutants
qa-mutants
qawriteshands-off
Use this when: surviving mutants need closing with real tests
QA mutants (kill the survivors)
Close surviving mutants by adding tests, lifting the codebase to the 95% covered-MSI nightly gate. The executor for Epic #131. Read docs/agents/mutation-testing.md + the CLAUDE.md Mutation Testing Policy first — this skill is that policy, automated.
Two modes
- Interactive (
/qa-mutants, a dev driving) — kill survivors on the current branch, then report. You commit/PR; the skill never merges. Normal surface-then-confirm. - Autonomous (headless,
qa-mutants-autofix.ymlafter a red nightly) — run the full loop + merge policy below unattended.
The loop (autonomous)
- Read the survivors. From the failing
mutation (nightly full)run, or locally via the memory-safe escape hatchphp artisan mutate:diff --no-parallel(parallel mutation is CI-only — the block hook denies it). Tally byfile:line+ mutator. - Kill every one. This is the skill — see the discipline below. Account for each survivor; none left un-examined.
- Re-run mutation to verify, then fix the remainder.
- Repeat, capped at 5 iterations OR 120 minutes. Still < 95% at the cap → raise an issue documenting the remaining survivors, linked to #131.
The discipline (every survivor, one of two ways)
- Default — kill it. Add the assertion that pins the branch / boundary / value. A survivor you "can't be bothered" to test is a real gap, not an equivalent.
- DTO &
casts()mutants are killable, not ignorable.RemoveStringCast/RemoveIntegerCast/RemoveBooleanCastinfrom*()hydrators and thecasts()method have observable behaviour (HTTP/JSON arrives as strings) — hydrate from a string and assert the strict resulting type (->toBe(15), not'15'). Kill before reaching for an ignore. - Last resort — reason-ignore a genuine equivalent, with the targeted
// @pest-mutate-ignore: Mutatorform + a one-line why (own line above the statement; doesn't reachif/foreachopeners,break/continue, mid-chain). An un-reasoned ignore is rubber-stamping — the bar is high. - Never lower a threshold; never contort production code to suit the tool. If a mutant reveals a real bug, fix the code and add the covering test.
Auto-merge bar (autonomous mode only — maintainer-approved)
The safety crux: a fixer optimising for "go green" can re-create a false-green with tautological tests or over-ignoring. So auto-merge only when there's nothing to second-guess:
Auto-merge iff ALL hold:
- every targeted survivor killed by an added assertion,
- zero new
@pest-mutate-ignore, - the diff is tests-only (no
app/changes), - the post-fix mutation run actually reports ≥ 95%,
composer ci:checkis green.
Otherwise open a PR for review — any of: an ignore was needed (equivalence is a judgement call), production code had to change, or only partial progress. Budget exhausted without 95% → raise an issue (don't merge a half-fix).
Done when
Autonomous: the nightly is back to ≥ 95% covered-MSI and either auto-merged (bar met) or a PR/issue is open with the residual. Interactive: survivors on the branch are resolved and reported, composer ci:check green, left for the dev to ship.
Not in
check-everything: this is an autonomous, codebase-wide, auto-merging daemon — too heavy for a per-change health sweep, andqa-testsalready owns the interactive pre-PR mutation review there.