- Home
- Skills
- Testing & Debugging
- Mutation-Verified Test Generator (Jest, Vitest, pytest)
More screenshots
Works with the AI Agents you already use
Mutation-Verified Test Generator (Jest, Vitest, pytest)
Writes tests for your code, then proves they catch bugs with a mutation check, and strengthens them until they do.
$12
Mutation-Verified Test Generator (Jest, Vitest, pytest)
Example session with this skill installed
Write tests for billing/invoice.py and check that they would actually catch bugs.Write tests for billing/invoice.py and check that they would actually catch bugs.
- Read your context and instructions
- Compiled the mutation-verified test generator
Test plan: billing/invoice.py (whole module)
Framework: pytest · Test file: tests/test_invoice.py (extending existing)
Style: mirror tree, flat test_ functions, plain assert, no mocks
| # | Behaviour | Kind | Input → expected |
|---|---|---|---|
| 1 | Commas and $ stripped | happy path | "1,234.50" → 1234.50; "$12" → 12.00 |
| 2 | Whitespace trimmed, quantized to cents | happy path | " 7.5 " → Decimal("7.50") (str checked) |
| 3 | Half-up rounding | boundary | "0.005" → 0.01; "0.004" → 0.00; "2.675" → 2.68 |
| 4 | Zero accepted | boundary | "0" → 0.00 |
| 5 | Empty / blank / bare $ | error path | "", " ", "$" → ValueError "empty amount" |
| 6 | Junk | error path | "abc", "1.2.3" → ValueError "not a number" |
| 7 | Negative | error path | "-5", "$-0.01" → ValueError "negative" |
| 8 | No fee within grace | boundary | days 0, 5 → 0.00 |
| 9 | Fee starts day 6 | boundary | 100 @ 6 days → 1.00, @ 10 → 5.00 |
| 10 | Cap at 25% | boundary | 100 @ 29 → 24.00, @ 30 → 25.00, @ 31/100 → 25.00 |
| 11 | Fee rounding | boundary | 0.50 @ 6 days → 0.01 |
| 12 | Discount tiers | boundary | 1, 19 → 0; 20, 99 → 0.05; 100, 500 → 0.10 |
| 13 | Total with VAT | happy path | 10×2 → 24.00; 10×1 → 12.00 |
| 14 | Tax exempt | happy path | 10×2 exempt → 20.00 |
| 15 | Discounts in total | boundary | 10×19 → 228.00; 10×20 → 228.00; 10×100 → 1080.00 |
| 16 | Late fee in total (on VAT-inclusive net) | happy path | 10×2, 10 days → 25.20; exempt → 21.00 |
| 17 | Total rounding | boundary | 0.335×1 exempt → 0.34 |
| 18 | Invalid units | error path / boundary | units 0, -1 → ValueError; units 1 accepted |
Mocking: nothing, since these are pure functions.
Not covered: none.
Possible source issue to check: "NaN" or "Infinity" passed to parse_amount may raise decimal.InvalidOperation instead of ValueError. I'll check this rather than assume it.
Next I'm scoring the existing test as it stands:
▸ Tool output: mutation check
$ python3 ~/.claude/skills/mutation-verified-test-generator/scripts/mutate.py --target billing/invoice.py --max-mutants 20 -- .venv/bin/python -m pytest tests/test_invoice.py -q -p no:cacheprovider
Mutants: 29 candidates, 20 selected (cap 20).
Baseline: running tests on the unmodified code…
Baseline passed in 0.2s. Timeout per mutant: 30s.
Backups: /var/folders/b1/327sn4cn0t1fb223h0pdsn140000gn/T/mvtg-backups (removed after a verified restore; if this process is killed, run again with --restore).
[ 1/20] done
[ 2/20] done
[ 3/20] done
[ 4/20] done
[ 5/20] done
[ 6/20] done
[ 7/20] done
[ 8/20] done
[ 9/20] done
[10/20] done
[11/20] done
[12/20] done
[13/20] done
[14/20] done
[15/20] done
[16/20] done
[17/20] done
[18/20] done
[19/20] done
[20/20] done
Mutation score: 9/20 (45.0%)
killed 9
killed (timeout) 0
survived 11
(re-run with --show-mutants to see them, once the new tests are written)
Restore check:
OK billing/invoice.py sha256 and modification time match original
The existing test scores 45% (9/20). Before writing tests I'll check the NaN/Infinity suspicion:
▸ Write tests/test_invoice.py
▸ Tool output: tests
$ .venv/bin/python -m pytest tests/test_invoice.py -q -p no:cacheprovider
.................................................... [100%]
52 passed in 0.03s
All 52 pass on the unmodified code. Running the first-draft mutation check:
▸ Tool output: mutation check
$ python3 ~/.claude/skills/mutation-verified-test-generator/scripts/mutate.py --target billing/invoice.py --max-mutants 20 --show-mutants -- .venv/bin/python -m pytest tests/test_invoice.py -q -p no:cacheprovider
Mutants: 29 candidates, 20 selected (cap 20).
Baseline: running tests on the unmodified code…
Baseline passed in 0.1s. Timeout per mutant: 30s.
Backups: /var/folders/b1/327sn4cn0t1fb223h0pdsn140000gn/T/mvtg-backups (removed after a verified restore; if this process is killed, run again with --restore).
[ 1/20] M1 killed billing/invoice.py:20 boundary if value < 0: -> if value <= 0:
[ 2/20] M2 killed billing/invoice.py:20 negate if value < 0: -> if value >= 0:
[ 3/20] M3 killed billing/invoice.py:22 return return value.quantize(CENT, rounding=ROUND_HALF_UP) -> return None
[ 4/20] M4 survived billing/invoice.py:27 boundary if days_late <= GRACE_DAYS: -> if days_late < GRACE_DAYS:
[ 5/20] M5 killed billing/invoice.py:27 negate if days_late <= GRACE_DAYS: -> if days_late > GRACE_DAYS:
[ 6/20] M6 killed billing/invoice.py:28 return return Decimal("0.00") -> return None
[ 7/20] M7 killed billing/invoice.py:29 arithmetic rate = (days_late - GRACE_DAYS) * DAILY_LATE_RATE -> rate = (days_late + GRACE_DAYS) * DAILY_LATE_RATE
[ 8/20] M8 killed billing/invoice.py:29 arithmetic rate = (days_late - GRACE_DAYS) * DAILY_LATE_RATE -> rate = (days_late - GRACE_DAYS) / DAILY_LATE_RATE
[ 9/20] M9 survived billing/invoice.py:30 boundary if rate > MAX_LATE_FEE_RATE: -> if rate >= MAX_LATE_FEE_RATE:
[10/20] M10 killed billing/invoice.py:30 negate if rate > MAX_LATE_FEE_RATE: -> if rate <= MAX_LATE_FEE_RATE:
[11/20] M11 killed billing/invoice.py:37 boundary if units >= 100: -> if units > 100:
[12/20] M12 killed billing/invoice.py:37 negate if units >= 100: -> if units < 100:
[13/20] M13 killed billing/invoice.py:38 return return Decimal("0.10") -> return None
[14/20] M14 killed billing/invoice.py:39 boundary if units >= 20: -> if units > 20:
[15/20] M15 killed billing/invoice.py:39 negate if units >= 20: -> if units < 20:
[16/20] M16 killed billing/invoice.py:41 return return Decimal("0") -> return None
[17/20] M17 killed billing/invoice.py:45 boolean tax_exempt: bool = False) -> Decimal: -> tax_exempt: bool = True) -> Decimal:
[18/20] M18 killed billing/invoice.py:48 arithmetic gross = unit_price * units -> gross = unit_price / units
[19/20] M19 killed billing/invoice.py:49 arithmetic net = gross - gross * volume_discount(units) -> net = gross - gross / volume_discount(units)
[20/20] M20 killed billing/invoice.py:51 arithmetic net = net + net * VAT_RATE -> net = net + net / VAT_RATE
Mutation score: 18/20 (90.0%)
killed 18
killed (timeout) 0
survived 2
Survivors:
M4 billing/invoice.py:27 boundary: if days_late <= GRACE_DAYS: -> if days_late < GRACE_DAYS:
M9 billing/invoice.py:30 boundary: if rate > MAX_LATE_FEE_RATE: -> if rate >= MAX_LATE_FEE_RATE:
Restore check:
OK billing/invoice.py sha256 and modification time match original
▸ Tool output: mutation check
$ python3 ~/.claude/skills/mutation-verified-test-generator/scripts/mutate.py --target billing/invoice.py --max-mutants 29 --show-mutants -- .venv/bin/python -m pytest tests/test_invoice.py -q -p no:cacheprovider 2>&1 | grep -v -E "^\[.*killed "
Mutants: 29 candidates, 29 selected (cap 29).
Baseline: running tests on the unmodified code…
Baseline passed in 0.2s. Timeout per mutant: 30s.
Backups: /var/folders/b1/327sn4cn0t1fb223h0pdsn140000gn/T/mvtg-backups (removed after a verified restore; if this process is killed, run again with --restore).
[ 4/29] M4 survived billing/invoice.py:27 boundary if days_late <= GRACE_DAYS: -> if days_late < GRACE_DAYS:
[ 9/29] M9 survived billing/invoice.py:30 boundary if rate > MAX_LATE_FEE_RATE: -> if rate >= MAX_LATE_FEE_RATE:
Mutation score: 27/29 (93.1%)
killed 27
killed (timeout) 0
survived 2
Survivors:
M4 billing/invoice.py:27 boundary: if days_late <= GRACE_DAYS: -> if days_late < GRACE_DAYS:
M9 billing/invoice.py:30 boundary: if rate > MAX_LATE_FEE_RATE: -> if rate >= MAX_LATE_FEE_RATE:
Restore check:
OK billing/invoice.py sha256 and modification time match original
Final checks: confirm the source is restored, then run the full suite.
I added 51 tests to tests/test_invoice.py. To check they catch bugs, I planted 29 small bugs ("mutants") in billing/invoice.py, one at a time, and the tests caught 27. The other two can't be caught by any test, because they never change the result. The old single test caught 9 of 20.
Source restored: billing/invoice.py is back byte-for-byte. The mutation script's checksum and timestamp check passed after every run, and git diff --exit-code on the file is clean. The only changed file is tests/test_invoice.py. The full suite passes: 52 tests.
Tests added
| Area | Covers |
|---|---|
parse_amount | Stripping commas, $ and spaces; output always has two decimals; rounding half up ("0.005" → 0.01, "2.665" → 2.67); zero accepted; empty, blank or bare $ rejected; junk ("1.2.3", "12$") rejected; negatives rejected ("-0.01", "$-0.01") |
late_fee | No fee for days −1, 0, 1 and 5; 1% a day from day 6; capped at 25% from day 30 (days 29, 30, 31, 100 checked); fee scales with the amount; rounding (0.50 at 6 days → 0.01) |
volume_discount | Each tier at its edges: 1/19 → 0, 20/21/99 → 5%, 100/500 → 10% |
invoice_total | One unit with VAT; tax-exempt; discounts at 19, 20 and 100 units, with and without VAT; late fee with VAT, tax-exempt, and within grace; final rounding; units 0 and −1 rejected |
Mutation score
| Run | Tests | Score | Killed | Killed (timeout) | Survived |
|---|---|---|---|---|---|
| Existing tests | 1 | 45.0% | 9 | 0 | 11 |
| First draft | 52 | 90.0% | 18 | 0 | 2 |
| All 29 mutants (extra run) | 52 | 93.1% | 27 | 0 | 2 |
By default the check only runs 20 of the 29 possible mutants. Each run took about 0.1s, so I also ran all 29. There was no strengthening round: the two that survived can't be caught by any test.
Test command: .venv/bin/python -m pytest tests/test_invoice.py -q -p no:cacheprovider
The two survivors
| Id | Location | Change | Why no test catches it |
|---|---|---|---|
| M4 | billing/invoice.py:27 | days_late <= GRACE_DAYS → < | On day 5 the fee becomes amount × 0, which is still 0.00. The only visible difference is -0.00 for a negative amount, which nothing in the code passes. |
| M9 | billing/invoice.py:30 | rate > MAX_LATE_FEE_RATE → >= | At exactly 25% it swaps 0.25 for 0.25, so the result is the same. |
Bug found (source not changed)
parse_amountlets some invalid input through with the wrong error type. Its docstring says it rejects junk, but"NaN","Infinity"and"sNaN"raisedecimal.InvalidOperationinstead ofValueError. Code that catchesValueErroraround this function would miss them. They pass theDecimal()parse and fail later, in the< 0check or the rounding step. I didn't add a test for this, so the suite stays green. One fix is to checkvalue.is_finite()after parsing and raiseValueErrorif it isn't. I can make that fix, or add the test marked as an expected failure (xfail(strict=True)).
Behaviour worth confirming
- The late fee is charged on the total after discount and VAT, so VAT gets a late fee on it too. The tests treat this as intended (
10 × 2units, 10 days late →25.20). If the fee should be on the pre-VAT amount, that test will need changing.
Connects securely to your tools. The creator never sees your data.
About this skill
The problem
Generated tests usually pass. That's the problem: a test that passes on correct code tells you nothing until you know it would fail on broken code. A suite can execute every line and still miss an off-by-one in a discount tier, an inverted cap, or a deleted validation call. Line coverage can't see that. Mutation testing can.
What this skill does differently
It doesn't just claim the tests are good: it measures it. After writing tests, the agent runs a mutation check. It makes small, realistic bugs in your source file one at a time (< → <=, + → -, && → ||, true → false, a return value blanked, a call removed), runs your tests after each, and counts how many were caught. Survivors point at exact gaps. The agent adds or tightens tests for them, re-runs the check, and explains any survivor that genuinely can't be caught.
Tests are written before the mutation check runs. When the agent scores your existing tests, it sees only the score, not the list of planted bugs. The first-draft score therefore measures tests written from the code's behaviour, not tests tailored to known mutants.
Who it's for
Developers and teams who want tests they can trust, especially for logic where boundaries matter: pricing, billing, permissions, validation, parsing, date and limit handling. Also useful for checking whether an existing suite would actually catch a regression in a file before you refactor it.
What the agent does, step by step
- Detects your language, test framework, test layout, naming, assertion and mocking style, and the narrowest command that runs the relevant tests.
- Checks there's a restore point: a clean
git statusfor the target, or the bundled script's own backup. - Reads the target and plans the tests: happy paths, boundaries, edge cases, error paths, and what will be faked and why.
- If tests already exist, scores them first (score only).
- Writes tests in your project's style and makes them pass on the unmodified code. If a test reveals a likely bug in the source, it reports it and doesn't change your source.
- Runs the mutation check, strengthens tests against survivors (up to two rounds), and re-runs.
- Verifies your source file is restored byte-for-byte, then runs your full test suite once.
- Reports: tests added and what each covers, the score for each run, every surviving mutant with an explanation, and how the restore was verified.
Example output
From a pytest project (an invoicing module with late fees, a grace period, a fee cap and discount tiers):
| Run | Tests | Mutation score |
|---------------------|-------|----------------|
| Existing tests only | 1 | 45% (9/20) |
| First draft | 52 | 90% (18/20) |
| All 29 mutants | 52 | 93% (27/29) |
Survivors (both explained as equivalent, so no test can catch them):
days_late <= GRACE_DAYS → < (at exactly day 5 the fee is 0.00 either way)
rate > MAX_LATE_FEE_RATE → >= (capping 25% to 25% changes nothing)
Restore check:
OK billing/invoice.py sha256 and modification time match original
Bug found (source not changed):
parse_amount("NaN") raises decimal.InvalidOperation instead of ValueError
These are examples, not promises. In testing, scores rose from 25% to 95% (TypeScript + Vitest), 50% to 100% (JavaScript + Jest) and 45% to 90% (Python + pytest) on small sample projects. On a real open-source TypeScript CLI tested with node:test, two files with decent existing tests went from about 82% to 100% each. Your results depend on your code and your existing tests.
How long it takes
Plan on a few minutes per file. The skill works on one source file per run by default. Each run of the check takes roughly (your test command's run time) × (number of mutants, 20 by default), so keep the test command narrow (the skill does this for you). If you name several files, the agent finishes and reports one before starting the next.
Safety, and what runs on your machine
- The bundled
scripts/mutate.py(Python 3.8+, standard library only) runs your project's own test command as a subprocess: once on unmodified code, then once per mutant. It runs without a shell, with a timeout. - It only ever writes to the target file (one mutant at a time, restored after every run, even if a run errors or is interrupted) and to its own backup folder in your system temp directory.
- It verifies the restore by SHA-256 and modification time. If the process is killed outright, the next run refuses to start until you restore from the backup with one command.
- It never overwrites an edit it didn't make.
- The script makes no network calls, reads no environment variables and installs nothing. The agent asks before installing anything and uses
npx --noso missing tools fail instead of downloading. - If Stryker or mutmut is already installed, the agent offers to use it instead, scoped to the target file.
- No Python? The agent applies the same mutation operators by hand with its own edit tool, under the same safety rules.
Supported
| Fully supported (tested) |
How to use
Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill passes our trust checks below.
Security scanned
Passed our 8-point scan before listing
Fresh listing
Recently published to Agensi
Money-back guarantee
Doesn't do what its page promises? You get your money back.
Trust & safety
Security scanned
Verified clean 1 day ago
- Passed all security checks, Safe to install
Requires