Mutation Test
Instructions
Run mutation testing for $ARGUMENTS (a use case ID like UC-XXX, or a technical task TT-XXX)
and report which surviving mutants prove a gap in the unit tests.
Line coverage proves a line ran. Mutation testing proves a line is asserted on: Stryker
mutates the source (flips > to >=, empties a block, swaps a boolean), re-runs the tests, and
a mutant that survives means no test noticed the behaviour changed.
When to use
- Automatically, as Step 3 of
/deliver-use-case, once implementation and unit tests pass and before the E2E and coverage gates run. Its inputs are complete at that point — E2E and integration tests are excluded from the Stryker runner, so later steps cannot change the score. - Standalone, to re-check a use case after adding tests.
DO NOT
- Mutate E2E tests, integration tests, generated code, or
prisma/ - Mutate the whole repository — only the files delivered for $ARGUMENTS (see Scope)
- Run the Testcontainers-backed integration tests under Stryker — each Stryker worker would boot its own container. Mutation testing targets unit-testable business logic
- Change production code to kill a mutant. The fix is a missing test assertion, not a code edit, unless the mutant reveals genuinely dead code
- Add
// Stryker disablecomments to make the score go up - Treat an equivalent mutant as a failure — classify it and move on
Nexa Rules Gate
Read and follow ${CLAUDE_PLUGIN_ROOT}/shared/readiness/NEXA_RULES_GATE.md.
Process
Step 1: Determine Scope
Mutate only the business logic delivered for $ARGUMENTS.
- Collect candidate files:
- Changed files since the delivery started:
git diff --name-only <base>where<base>is the rollback commit recorded by/deliver-use-case(orgit merge-base HEAD mainwhen invoked standalone), plus untracked files fromgit status --porcelain. - Cross-check against the implementation files listed in
docs/use_cases/$ARGUMENTS.mdanddocs/delivery/$ARGUMENTS-traceability.mdif present.
- Changed files since the delivery started:
- Keep only
.ts/.tsxfiles holding logic worth mutating: services, domain/business rules, validators, server actions, API route handlers, utilities. - Drop: test files,
page.tsx/layout.tsxshells with no logic, pure presentational components, type-only files, generated Prisma client, config files,messages/*.json.
If the filtered set is empty, stop and report NOT APPLICABLE — no mutable business logic delivered for $ARGUMENTS. That is not a failure.
Step 2: Ensure Stryker Is Installed
If stryker.config.json does not exist, bootstrap it (once per project):
npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
Create stryker.config.json from templates/stryker.config.json.
If vitest.config.ts declares a globalSetup (Testcontainers), also create
vitest.mutation.config.ts from templates/vitest.mutation.config.ts
— it reuses the main config without the container setup and without integration tests, so Stryker
workers run pure unit tests only.
Add to .gitignore if absent: reports/mutation/ and .stryker-tmp/.
Step 3: Run Stryker
Point --mutate at the scoped file list from Step 1:
npx stryker run --mutate "src/services/foo.ts,src/lib/bar.ts"
Read the score and the surviving mutants from reports/mutation/mutation.json.
If Stryker fails to start (no tests match, config error, missing runner), fix the configuration
and re-run once. If the unit tests themselves fail before mutation begins, stop — report
BLOCKED: unit tests failing, since mutation results are meaningless against a red suite.
Step 4: Classify Surviving Mutants
For each survivor, decide:
| Class | Meaning | Action |
|---|---|---|
| Test gap | The mutated behaviour is specified in the use case (an MSS step, alternative flow, or business rule) and no test asserts it | Report with the exact assertion that kills it |
| Equivalent | The mutant is semantically identical to the original (e.g. a boundary that is unreachable, a default that is always overwritten) | Report as equivalent, no action |
| Not worth killing | Logging, telemetry, defensive branch for an impossible state | Report as observation, no action |
For every Test gap, name the file, line, mutator, the spec item it maps to
(FR-XXX / BR-XXX / MSS step), and the concrete assertion that would kill it.
Step 5: Report
Write docs/delivery/$ARGUMENTS-mutation.md:
# Mutation Report: $ARGUMENTS
- **Mutation score:** NN.N% (killed M / total T)
- **Threshold:** 80%
- **Verdict:** PASS | PASS WITH OBSERVATIONS | FAIL
- **Scope:** [files mutated]
## Surviving Mutants
| File:Line | Mutator | Original → Mutated | Spec Item | Class | Killing Assertion |
|-----------|---------|--------------------|-----------|-------|-------------------|
## Observations
[equivalent and not-worth-killing mutants, one line each]
Verdict
- PASS — mutation score ≥ 80% on the scoped files AND no surviving mutant is classified as a Test gap on a business rule or MSS step.
- PASS WITH OBSERVATIONS — score ≥ 80%, survivors are only equivalent / not-worth-killing.
- FAIL — score < 80%, or any Test gap survivor maps to a business rule or MSS step.
The threshold is on the scoped files, not the repository.
Verification
The run is complete when all of these hold:
reports/mutation/mutation.jsonexists and its timestamp is from this run.- Every surviving mutant in that file appears in the report, classified.
docs/delivery/$ARGUMENTS-mutation.mdexists with a verdict.
Do not report a score you did not read out of mutation.json.