seed-skills/mutation-testing/SKILL.md
Run mutation testing with StrykerJS for TypeScript/JavaScript and mutmut for Python to measure whether your test suite actually catches bugs, enforce mutation score thresholds, and keep runs fast with incremental mode in CI.
npx skillsauth add PramodDutta/qaskills Mutation TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes an AI agent set up and interpret mutation testing: StrykerJS for JS/TS projects (Jest or Vitest runners) and mutmut for Python. Mutation testing injects small bugs (mutants) into source code and re-runs the tests — if the tests still pass, the mutant "survived" and your suite has a blind spot that line coverage never showed. Trigger this when a team claims high coverage but still ships regressions, when reviewing test suite quality, or when the user mentions Stryker, mutmut, or mutation score.
break thresholds, not aspirational targets. Start at your current score minus 2, fail the build below it, and ratchet up monthly. A threshold nobody enforces is documentation fiction.i < len to i <= len on an array that is never exactly full) produce identical behavior. Mark them ignored explicitly rather than chasing 100%.> → >= mutant is usually one boundary-value assertion in an existing test, not a new test file.npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker init
// stryker.config.json
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"testRunner": "vitest",
"mutate": ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.d.ts", "!src/generated/**"],
"coverageAnalysis": "perTest",
"reporters": ["html", "clear-text", "progress", "json"],
"thresholds": { "high": 85, "low": 70, "break": 65 },
"incremental": true,
"incrementalFile": ".stryker-tmp/incremental.json",
"timeoutMS": 10000,
"concurrency": 4
}
For Jest projects swap the runner:
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
// stryker.config.json (Jest variant)
{
"testRunner": "jest",
"jest": { "projectType": "custom", "configFile": "jest.config.js" },
"coverageAnalysis": "perTest",
"mutate": ["src/**/*.ts", "!src/**/*.test.ts"],
"thresholds": { "high": 85, "low": 70, "break": 65 }
}
Run it:
npx stryker run # full run, writes reports/mutation/mutation.html
npx stryker run --mutate src/pricing.ts # scope to one file while fixing survivors
Stryker output:
[Survived] ArithmeticOperator
src/pricing.ts:14:31
- return subtotal * (1 - discountRate);
+ return subtotal / (1 - discountRate);
Ran 3 tests, none failed.
The suite never asserts a discounted price with a nonzero rate. Kill it with a boundary assertion:
// src/pricing.test.ts
import { describe, expect, it } from 'vitest';
import { applyDiscount } from './pricing';
describe('applyDiscount', () => {
it('multiplies subtotal by the inverse discount rate', () => {
// 200 * (1 - 0.25) = 150; the division mutant would return 266.67
expect(applyDiscount(200, 0.25)).toBe(150);
});
it('returns the subtotal unchanged at rate 0', () => {
expect(applyDiscount(99.5, 0)).toBe(99.5);
});
});
Ignore a genuinely equivalent mutant inline instead of lowering the threshold:
// Stryker disable next-line EqualityOperator: loop bound is equivalent for empty input
for (let i = 0; i < items.length; i++) {
pip install mutmut pytest
# pyproject.toml
[tool.mutmut]
paths_to_mutate = ["src/"]
tests_dir = ["tests/"]
also_copy = ["conftest.py"]
mutmut run # mutates src/, runs pytest per mutant, caches results
mutmut results # summary: killed / survived / timeout / suspicious
mutmut show src.pricing.apply_discount__mutmut_3 # diff of one survivor
mutmut run --max-children 4 # parallel workers
Killing a Python survivor follows the same pattern — the mutant tells you the missing assertion:
# tests/test_pricing.py
import pytest
from src.pricing import apply_discount
def test_apply_discount_uses_multiplication():
# mutant changed * to /; 200 * 0.75 == 150, 200 / 0.75 == 266.67
assert apply_discount(200, 0.25) == 150
def test_apply_discount_rejects_rate_above_one():
# kills the mutant that removed the validation guard
with pytest.raises(ValueError):
apply_discount(100, 1.5)
Incremental Stryker on pull requests, full run nightly:
# .github/workflows/mutation.yml
name: mutation
on:
pull_request:
paths: ['src/**', 'stryker.config.json']
schedule:
- cron: '0 2 * * *'
jobs:
stryker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # incremental mode diffs against git history
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Restore incremental cache
uses: actions/cache@v4
with:
path: .stryker-tmp/incremental.json
key: stryker-incremental-${{ github.base_ref || 'main' }}
- name: Mutation test (incremental on PRs, full nightly)
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
npx stryker run --incremental
else
npx stryker run --force
fi
- uses: actions/upload-artifact@v4
if: always()
with:
name: mutation-report
path: reports/mutation/mutation.html
The thresholds.break: 65 in config makes stryker run exit non-zero below 65% — no extra scripting needed.
coverageAnalysis: "perTest" — Stryker then only runs the tests that cover each mutant, often a 10x speedup..stryker-tmp/incremental.json to CI cache, never to git.timeoutMS explicitly. Mutants that create infinite loops are killed by timeout; the default factor-based timeout misbehaves on very fast suites.mutmut run against a single module first: paths_to_mutate = ["src/billing.py"].*.test.ts, *.spec.ts, migrations, and codegen output in mutate globs.break to make a red build green. The threshold ratchets up, never down; fix the survivors or explicitly ignore equivalents inline.stryker.config.json, stryker.conf.mjs, or [tool.mutmut] section already exists in the repo.development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.