moonbuggy¶
Fast, agent-first mutation testing for Python.
This site is published at
jhamon.github.io/moonbuggy on every
merge to main.
Your test suite tells you which lines ran. It does not tell you whether anything would have noticed if those lines were wrong. Mutation testing answers the second question by breaking your code on purpose and seeing whether your tests complain.
$ moonbuggy
moonbuggy: 84 mutants across 3 files
moonbuggy: running coverage pass...
SURVIVED app/pricing.py:14 comparison_swap line=14 nearest_test=tests/test_pricing.py::test_discount tests_run=3 id=app/pricing.py:14:comparison_swap:0
KILLED app/pricing.py:15 constant_int line=15 nearest_test=- tests_run=3 id=app/pricing.py:15:constant_int:0
moonbuggy: KILLED=71 KILLED_BY_ERROR=0 NO_COVERAGE=3 SKIPPED=0 SURVIVED=12 SUSPICIOUS=1 TIMEOUT=0 cached=0 -> .moonbuggy/results.jsonl
Each SURVIVED line is a change moonbuggy made to your code that no test
noticed. That is a gap in your suite, named down to the line. A NO_COVERAGE
line is the other kind of gap: no test executes it at all.
Handing this to an agent takes no install and no setup: run
uv run --with moonbuggy moonbuggy -h, show it the help, and let it drive —
see the Quickstart.
Start here¶
If you have never used mutation testing, read the two pages in order. If you have, the quickstart alone is enough.
Getting started
Contributing
What makes this one different¶
It is built for a reader that greps. Output is one line per mutant, leading
with a status keyword, with key=value tokens after it. grep SURVIVED works
with no knowledge of the schema (grep NO_COVERAGE gets the lines no test
reaches), and the JSONL alongside it is the same data for
anything that wants to parse rather than scan. See
Reading the output.
It hands its findings to the next worker. moonbuggy export writes every
finding — survivors and uncovered lines — as one JSONL file whose records a
test-fixing agent (human or automated) can consume directly, with every id
a valid moonbuggy run <id> input to re-measure after the fix. See
The findings export.
It refuses rather than guesses. A wrong status is worse than no status: a
false SURVIVED looks exactly like a real finding and costs you an
investigation that ends nowhere. So a flaky test makes its mutants
SUSPICIOUS rather than confidently anything, an already-failing suite is
refused outright, and a mutation that cannot be applied is an error rather than
a survivor.
It only runs the tests that could possibly notice. A mutation on line 14 can only be caught by a test that executes line 14. One instrumented run of your suite builds that map, and each mutant then runs against a handful of tests instead of all of them. See Making runs fast.