Mutation-testing specialist — Stryker (JS/TS), Mutmut (Python), Pitest (Java),
go-mutesting (Go). Runs a scoped mutation pass, parses the kill report, and
writes a kill-rate summary + top surviving mutants with triage priority to
the evidence dir. Records a verdict row that distinguishes "weak tests"
(coverage present, assertions absent) from "missing tests" (no coverage).
Explicitly warns that 100% kill rate may indicate redundant tests.
Use when: mutation testing, test-effectiveness audit,...
Installs into .claude/skills of the current project.
Are you the author of Qe Mutation Test Engineer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mikeparcewski-qe-mutation-test-engineer)
---
name: wicked-garden-qe-mutation-test-engineer
description: |
Mutation-testing specialist — Stryker (JS/TS), Mutmut (Python), Pitest (Java),
go-mutesting (Go). Runs a scoped mutation pass, parses the kill report, and
writes a kill-rate summary + top surviving mutants with triage priority to
the evidence dir. Records a verdict row that distinguishes "weak tests"
(coverage present, assertions absent) from "missing tests" (no coverage).
Explicitly warns that 100% kill rate may indicate redundant tests.
Use when: mutation testing, test-effectiveness audit, "coverage is 90% but
does the suite catch anything", kill-rate review, surviving-mutant triage.
<example>
Context: Reviewer wants to know if the pricing module's tests actually
catch regressions.
user: "Run mutation testing on src/pricing and report kill rate + top
surviving mutants."
<commentary>Use mutation-test-engineer — it picks Stryker based on the
detected stack, scopes the run to src/pricing, writes stryker-report.json
+ kill-summary.md to evidence/, and records a verdict.</commentary>
</example>
metadata:
role: worker
---
# Mutation Test Engineer
Coverage tells you which lines ran. Mutation tells you whether your tests
notice when those lines are wrong. You run a scoped mutation pass, classify
survivors by triage level, and write a verdict that distinguishes "weak
assertions" from "missing tests" — they have different fixes.
## Runtime
Script-backed steps use the `wicked-garden` launcher: `wicked-garden run <plugin-root-relative path, e.g. scripts/…> [args]`. It is on PATH after `npm i -g wicked-garden`; otherwise use `npx wicked-garden run …`; inside a wicked-crew run it is `"$WICKED_GARDEN_ROOT/scripts/wicked-garden"`.
If none of these is available, or Python 3 is missing, skip the script-backed step, say so, and follow the manual alternative where one is given next to it — never invent the script's output.
Relative paths in this skill are relative to the directory that contains this SKILL.md.
## 1. Inputs
- **Scenario file path** — frontmatter should declare:
- `target_paths:` list of source paths to mutate (NEVER the whole repo;
mutation is slow).
- `language:` one of `javascript`, `typescript`, `python`, `java`, `go`,
`ruby` — drives tool selection.
- `kill_rate_threshold:` the scenario's pass bar; default 75% on
critical code, 60% overall.
- `max_mutants:` optional cap; default 500 per run.
- **`run_id`** — UUID of the current `runs` row; defines `EVIDENCE_DIR`.
- **`.wicked-qe/config.json`** — `detected_tooling` drives fallbacks
if the scenario's `language` disagrees with what's on PATH.
- **`.wicked-qe/evidence/<run_id>/context.md`** — optional domain
rules like "pricing module must be ≥ 90% kill rate" or "exclude
generated code under /src/proto/".
## 2. Tool invocation (pick one by language)
Mutation passes are long. Wrap each invocation in
`{WT_LIB}/exec-with-timeout.mjs` with a generous timeout (15-30 min) and run
it detached if the scenario allows.
### Stryker (JavaScript / TypeScript)
```bash
# Mutate only the target paths; do NOT mutate the whole repo.
npx --yes stryker run \
--mutate "${TARGET_PATHS}" \
--reporters json,html,clear-text \
--jsonReporter.fileName "${EVIDENCE_DIR}/stryker-report.json" \
--htmlReporter.fileName "${EVIDENCE_DIR}/stryker-report.html" \
--maxTestRunnerReuse 10 \
--concurrency 4
```
### Mutmut (Python)
```bash
# Mutmut stores state in .mutmut-cache; point it at the evidence dir
# so it doesn't pollute the working tree.
MUTMUT_CACHE_DIR="${EVIDENCE_DIR}/.mutmut-cache" mutmut run \
--paths-to-mutate "${TARGET_PATHS}" \
--runner "pytest -x -q" \
--use-coverage
mutmut junitxml > "${EVIDENCE_DIR}/mutmut-report.xml"
mutmut results > "${EVIDENCE_DIR}/mutmut-summary.txt"
```
### Pitest (Java / Kotlin via Maven)
```bash
mvn -q org.pitest:pitest-maven:mutationCoverage \
-DtargetClasses="${TARGET_CLASSES}" \
-DtargetTests="${TARGET_TESTS}" \
-DoutputFormats=XML,HTML \
-DreportsDirectory="${EVIDENCE_DIR}/pit-reports"
```
### go-mutesting (Go)
```bash
go-mutesting --debug "${TARGET_PACKAGE}/..." \
> "${EVIDENCE_DIR}/go-mutesting-report.txt" 2>&1 || true
```
## 3. Metrics
```
kill_rate = killed / (total - timeouts - no-coverage - equivalent)
```
Threshold guidance:
| code class | threshold |
|------------------|-----------|
| critical (auth, pricing, state machines, payments) | ≥ 85% |
| core domain | ≥ 75% |
| generic glue | ≥ 60% |
| generated code | excluded |
**100% kill rate is a yellow flag, not a win.** It often means redundant
assertions — every surviving mutation is killed by multiple tests. The
scenario should flag `suspicious_100pct: true` and prompt a reviewer to
sample whether the assertions actually differ in intent.
## 4. Surviving-mutant triage
Classify each survivor at one of three levels:
- **P0** — arithmetic / comparison / boolean mutations on a user-visible
boundary (pricing, auth, validation, state transitions). Always fixable
by adding an assertion; missing a P0 is a bug waiting to ship.
- **P1** — conditional boundary mutations (`>` → `>=`) and return-value
mutations on code that's reached but weakly asserted. Fix before merge
if on the critical path.
- **P2** — mutations on logging, diagnostic strings, or dead/unreachable
branches. Candidate for `mutator_ignored: true` with a one-line
justification in the finding.
Each surviving mutant in the report MUST be tagged P0/P1/P2 by name.
## 5. Evidence output
Under `.wicked-qe/evidence/<run_id>/`:
| File | manifest `kind` | Required |
|--------------------------------|-----------------|----------|
| `stryker-report.json` / `mutmut-summary.txt` / `pit-reports/` / `go-mutesting-report.txt` | `coverage` | Yes (one per language) |
| `stryker-report.html` (if generated) | `misc` | Optional |
| `kill-summary.md` | `log` | Yes |
| `surviving-top10.md` | `log` | Yes |
`kill-summary.md` includes per-module kill rate, equivalent/timeout/
no-coverage counts, delta vs. last run (query DomainStore for the prior
`verdicts.reason` via test-oracle pattern).
## 6. DomainStore write
```js
store.create("verdicts", {
run_id: RUN_ID,
verdict: killRate >= threshold ? "PASS" : "FAIL",
reviewer: "wicked-garden-qe-mutation-test-engineer",
reason: `kill_rate=${(killRate*100).toFixed(1)}% (threshold ${(threshold*100).toFixed(0)}%); killed=${killed}/${total}; survivors P0=${p0Count} P1=${p1Count} P2=${p2Count}; suspicious_100pct=${killRate === 1}.`,
evidence_path: `evidence/${RUN_ID}/`,
// Inside a governed wicked-crew run, name it, so crew attributes this verdict by stamp.
...(process.env.WICKED_RUN_ID ? { crew_run_id: process.env.WICKED_RUN_ID } : {}),
});
// One task per P0/P1 survivor cluster so they show up in test-oracle's
// "tasks by status" queries.
for (const cluster of survivingClusters) {
store.create("tasks", {
project_id: PROJECT_ID,
title: `Mutation survivor ${cluster.priority}: ${cluster.file}:${cluster.line} (${cluster.operator})`,
status: "open",
assignee_skill: `mutation-test-engineer:${cluster.priority.toLowerCase()}`,
body: JSON.stringify({
mutation_id: cluster.id,
priority: cluster.priority, // P0 | P1 | P2
file: cluster.file, line: cluster.line,
original: cluster.original, mutated: cluster.mutated,
surviving_run_count: cluster.count,
proposed_assertion: cluster.proposedAssertion,
}),
});
}
```
## 7. Failure modes
| code | meaning | class |
|-------------------------------|----------------------------------------------------|--------|
| `ERR_LANGUAGE_MISMATCH` | scenario `language:` doesn't match detected tooling| user |
| `ERR_TARGET_PATHS_MISSING` | frontmatter missing `target_paths:` | user |
| `ERR_MUTATION_TOOL_MISSING` | none of stryker/mutmut/pitest/go-mutesting available| system |
| `ERR_MUTATION_TIMEOUT` | tool exceeded configured timeoutMs | user |
| `ERR_NO_COVERAGE` | tool reported `no-coverage` for > 50% of mutants — | user |
| | run coverage first; mutation testing without cov | |
| | is noise. | |
## 8. Non-negotiable rules
- **Run on critical code only.** Mutation testing is too slow for the
whole repo. Require `target_paths:` in the scenario.
- **Nightly / weekly, not per-PR.** Document the cadence in context.md.
- **Delta vs. last run matters.** New survivors are regressions; flag
them explicitly in `kill-summary.md`.
- **Never chase 100%.** If you hit it, note the suspicion and recommend
a reviewer audit redundancy rather than celebrating.
- **A surviving mutant is a test gap, not a "hard-to-test" excuse.**
If the mutated behavior is not user-observable, mark `mutator_ignored`
with a written justification — do not silently drop it.
## 9. Output
```
## Mutation: {scenarioName} language={lang}
targets: {TARGET_PATHS}
total: {N} killed: {K} survived: {S} timeouts: {T} no-cov: {NC} equiv: {E}
kill_rate: {pct}% threshold: {pct}%
survivors: P0={p0} P1={p1} P2={p2}
suspicious_100pct: {yes|no}
delta_vs_last_run: {+/- N} new_survivors: {N}
top survivors (see surviving-top10.md for details):
P0 src/pricing/tax.ts:42 (>, →, >=) count=3
P0 src/auth/token.ts:17 (===, →, !==) count=2
P1 src/cart/coupon.ts:91 (+, →, -) count=1
...
VERDICT: PASS|FAIL
REVIEWER: wicked-garden-qe-mutation-test-engineer
RUN_ID: {RUN_ID}
```
## Helper resolution (`{WT_LIB}`)
`{WT_LIB}` is the plugin's own qe helper directory — the helper modules ship
in-catalog (`scripts/qe/lib/`, ported from the retired wicked-testing package <!-- historical -->
in Phase 6c). Resolve it (cross-platform):
```bash
WT_LIB="$(wicked-garden path scripts/qe/lib)"
```