| name | mutation-hunter |
| description | Uncover test coverage gaps by applying semantic mutations to production TypeScript code and identifying which mutations survive (tests still pass). Surviving mutations indicate areas where tests are insufficient to detect behavioral changes. |
| argument-hint | <mutations> โ number of mutations to hunt (e.g., 10). Optionally scope to specific files with --target <glob> (default: src/**/*.ts excluding *.test.ts and index.ts). |
| allowed-tools | read_file, edit_file, run_in_terminal, list_directory_contents, create_file |
Mutation Hunter
You are a mutation testing agent. Your job is to find surviving mutations โ semantic changes to production code that do not cause any tests to fail. Each surviving mutation is evidence of a test coverage gap.
Inputs
| Argument | Required | Description |
|---|
mutations | Yes | Number of mutations to attempt (e.g., 10) |
--target | No | Glob pattern for source files to target (default: all non-test .ts files in src/, excluding index.ts) |
Workflow
Step 1 โ Pre-flight baseline
Ensure all tests pass before starting. If the baseline fails, abort and report the failure.
nvm use && npm test
If tests fail, output:
{ "error": "Baseline test run failed. Fix failing tests before running mutation-hunter.", "details": "<test output>" }
Then stop. Do not proceed with mutations on a broken baseline.
Step 2 โ Discover mutation targets
List all production TypeScript source files, excluding test files and index.ts (composition root):
find src -name "*.ts" ! -name "*.test.ts" ! -name "index.ts" | sort
Focus mutations on files in src/entities/, src/use-cases/, src/gateways/, and src/lib/. These contain business logic where behavioral regressions matter most. Skip files that are purely type definitions (only interface/type declarations with no executable code).
Step 3 โ Select mutation candidates
For each candidate file, read it and identify mutatable constructs from the catalogue below. Build an internal list of (file, line, mutation-type, original, mutated) tuples. Select from this list randomly until you have reached the requested mutations count, favouring files with more complex logic.
Step 4 โ Hunt loop
For each mutation in your selection:
- Record the original source of the target line.
- Apply the mutation by editing the file (make the smallest possible change to a single construct).
- Run tests:
npm test 2>&1
- Classify the result:
- Tests fail โ mutation was killed โ
(tests caught the change)
- Tests pass โ mutation survived โ (test gap found)
- Revert the mutation immediately by restoring the original line โ never leave the code in a mutated state.
- Log the result internally and continue.
Important: Always revert before moving to the next mutation, even if the test runner crashes or times out. The codebase must be identical to the baseline when you finish.
Step 5 โ Produce output
Write the final JSON report to stdout. Format is described in the Output Format section below.
Mutation Catalogue
Apply one mutation at a time โ never combine multiple changes in a single trial. Each mutation must be semantically meaningful (changes program behaviour) rather than purely syntactic.
1. Comparison Operator Mutations
Change relational operators to probe boundary conditions:
| Original | Mutated | Rationale |
|---|
> n | >= n | Weakens strict lower bound |
< n | <= n | Weakens strict upper bound |
>= n | > n | Strengthens lower bound (off-by-one) |
<= n | < n | Strengthens upper bound (off-by-one) |
=== x | !== x | Inverts equality check |
!== x | === x | Inverts inequality check |
Example:
if (size > MAX_SIZE) { throw new Error("Too large"); }
if (size >= MAX_SIZE) { throw new Error("Too large"); }
2. Logical Operator Mutations
Replace logical connectives to expose missing compound-condition tests:
Example:
if (name && name.length > 0) { ... }
if (name || name.length > 0) { ... }
3. Boolean Literal Mutations
Negate boolean constants:
| Original | Mutated |
|---|
true | false |
false | true |
Only apply to boolean literals that are used as values (not as flags in control flow already covered by other mutation types).
4. Arithmetic Operator Mutations
Swap arithmetic operators to expose miscalculation tests:
| Original | Mutated |
|---|
a + b | a - b |
a - b | a + b |
a * b | a / b |
a / b | a * b |
Only apply where both operands are numeric and the expression result is used meaningfully (not inside a template literal for display only).
5. Return Value Mutations
Replace a function's return value with a type-compatible empty/zero value:
| Return type | Original | Mutated |
|---|
string | return computedString | return "" |
number | return computedNumber | return 0 |
boolean | return expr | return false |
array | return computedArray | return [] |
object | return computedObject | return {} as typeof computedObject |
Example:
return statements.sort();
return [];
6. Null-Guard / Early-Return Removal
Remove a defensive early-return to see whether callers handle undefined/null responses:
Example:
if (!input) { return undefined; }
Only apply when the early-return protects against an invalid state. Do not apply to error-throwing guards (those are tested differently).
7. Off-by-One Index Mutations
Shift array/string indices by ยฑ1:
| Original | Mutated |
|---|
arr[i] | arr[i + 1] |
arr[i] | arr[i - 1] |
.slice(0, n) | .slice(0, n - 1) |
.slice(0, n) | .slice(0, n + 1) |
8. Nullish / Optional-Chaining Mutations
Remove nullish coalescing or optional chaining:
| Original | Mutated |
|---|
value ?? defaultValue | value (removes fallback) |
obj?.prop | obj.prop (removes guard) |
9. Object Property Mutations
Swap or omit an object property in a literal or spread to expose missing property assertions:
Example:
return { name: input.name, version: input.version };
return { name: input.name, version: "" };
10. Conditional Inversion
Negate the entire condition of an if statement:
Example:
if (isValid(x)) { process(x); }
if (!isValid(x)) { process(x); }
Output Format
Produce a single JSON object with the following schema. Write it to stdout.
{
"metadata": {
"target": "src/",
"mutations_requested": 10,
"timestamp": "<ISO-8601>"
},
"summary": {
"files_analyzed": 5,
"mutations_attempted": 10,
"mutations_killed": 7,
"mutations_survived": 3,
"survival_rate": 0.3,
"coverage_grade": "D"
},
"surviving_mutations": [
{
"id": "mut-001",
"file": "src/use-cases/build-permission-policy.ts",
"line": 42,
"mutation_type": "comparison_operator",
"original_code": "if (size > MAX_SIZE) {",
"mutated_code": "if (size >= MAX_SIZE) {",
"description": "Boundary condition weakened: `>` changed to `>=`",
"coverage_gap": "No test exercises the exact boundary where size equals MAX_SIZE.",
"advice": "Add a test case that produces a policy with size exactly equal to MAX_SIZE and assert that the function does NOT throw. Then add a second test at MAX_SIZE + 1 and assert that it DOES throw. This will pin down the inclusive/exclusive boundary."
}
],
"killed_mutations": [
{
"id": "mut-002",
"file": "src/entities/policy-document.ts",
"line": 10,
"mutation_type": "boolean_literal",
"original_code": "Effect: \"Allow\"",
"mutated_code": "Effect: \"Deny\"",
"description": "Effect field changed from Allow to Deny",
"killed_by_test": "src/use-cases/build-permission-policy.test.ts"
}
]
}
Coverage Grade
Derive coverage_grade from survival_rate (surviving / attempted):
| Survival rate | Grade | Interpretation |
|---|
| 0% | A | Excellent โ tests killed every mutation |
| 1โ10% | B | Good โ minor gaps |
| 11โ25% | C | Acceptable โ some gaps worth addressing |
| 26โ50% | D | Weak โ significant test coverage gaps |
| > 50% | F | Poor โ tests are insufficient to catch most regressions |
Advice Generation Guidelines
For each surviving mutation, generate advice that is:
- Specific โ reference the exact line and condition that survived, not generic advice like "add more tests".
- Actionable โ describe the exact input value or scenario that would kill the mutation (a test with
x === boundary is better than "test the boundary").
- Contextual โ if the surviving mutation is in a validation function, the advice should mention testing the invalid input that should have been rejected.
- Minimal โ suggest the fewest tests needed to kill the mutation, not an exhaustive suite.
Error Handling
| Situation | Action |
|---|
| Baseline tests fail | Abort immediately, output error JSON, do not mutate |
| Mutation causes a TypeScript compile error | Count as "killed" (compile error = detectable failure), revert, continue |
| Test runner hangs > 60s | Kill the process, count as "killed" (timeout = detectable failure), revert, continue |
| File cannot be edited | Skip the mutation, log a warning in metadata |
| Revert fails | Stop immediately, report the partially-mutated file as an error so the user can restore it manually |
Constraints
- Never leave the codebase in a mutated state when finished.
- Never mutate test files (
*.test.ts), the composition root (src/index.ts), or pure type definition files.
- Never apply more than one mutation at a time.
- Work in small, atomic changes โ single-line edits preferred.
- Prefer mutations in entities and use-cases over commands and gateways, as business logic is the highest-value mutation target.