AI News HubLIVE
In-site rewrite4 min read

Show HN: Crucible – your AI wrote the tests, so who tested the tests?

Crucible is an adversarial test-hardening tool that uses mutation testing to find defects that AI-written tests miss. It provides a free score command to evaluate your suite, then an adversarial loop where a Tester writes tests, mutmut finds survivors, and a Critic writes targeted tests. The tool produces machine-verifiable receipts and runs on Python/pytest projects.

SourceHacker News AIAuthor: jott2121

Notifications You must be signed in to change notification settings

Fork 0

Star 0

Add this Action to an existing workflow or create a new one

View on Marketplace

BranchesTags

Open more actions menu

Folders and files

NameName

Last commit message

Last commit date

Latest commit

History

21 Commits

21 Commits

.claude/skills/harden-tests

.claude/skills/harden-tests

.github

.github

.repo-sentinel

.repo-sentinel

docs

docs

experiments

experiments

scripts

scripts

src/crucible

src/crucible

tests

tests

.gitignore

.gitignore

LICENSE

LICENSE

README.md

README.md

action.yml

action.yml

pyproject.toml

pyproject.toml

Repository files navigation

Your AI wrote the tests. Who tested the tests?

Recorded live — every command really ran, and every number in it is read back from that run's own receipt. Sped up for viewing; replay the raw cast at real speed. Full landing page: jott2121.github.io/crucible

That module has 7 passing tests and 97% line coverage. Mutation testing injects 71 real defects. The suite kills 46. 25 survive — twenty-five real bugs walking straight through a green build.

crucible kills 24 of the 25, and then does the more important thing: in two rounds the Critic wrote tests that failed on pristine code, and crucible threw them out rather than bank a kill it hadn't earned. The loop ends dry with one mutant still standing, and says so. Mutation score 65% → 99%; line coverage never moves off 97% the entire time. Coverage was never the thing measuring your safety.

An earlier run on the same module killed all 25 (clean). Same tool, same module, different day — model nondeterminism is real, and the receipts record both rather than only the flattering one.

Put the number in your own PRs

The diagnose costs nothing and calls no model. Point the Action at a module and every pull request tells you how many real bugs your suite would actually have caught:

  • uses: Jott2121/crucible@v1

with: module: yourpkg/yourmodule.py # omit if your repo already configures [tool.mutmut] fail-under: "70" # optional: red the build below this

It comments the score and names the survivors. No model, no API key, $0 — the number that embarrasses a coverage badge is free to compute, and you should not need a subscription to be told the truth about your own tests.

For the badge, set badge-file: mutation.json, publish it, and point shields.io at it. The payload lives in your repo — there is no badge service of mine sitting in the middle of your CI, and no uptime I owe you. The badge at the top of this page is generated exactly that way, by this Action, on this repo:

[![mutation](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/YOU/REPO/badges/mutation.json)](...)

Or just run it locally, on any repo, right now:

crucible score . --module yourpkg/yourmodule.py --coverage 97 97% line coverage, but 25 of 71 injected defects SURVIVED this suite (46 killed, mutation score 65%).

Coverage measures what ran, not what would be caught. Mutation testing injects real defects and counts how many your suite kills — and AI-written suites routinely leave survivors. crucible closes the loop: a Tester agent writes tests, mutmut finds the survivors — injected defects no test caught — and a Critic agent is handed the named survivors and writes tests to kill exactly those. Every verdict is mechanical: pytest kills the mutant or it survives. No model ever grades model output.

Quickstart — the free first win (no model, no keys, one command)

Find out what your existing tests actually miss, on your own repo:

pip install "crucible @ git+https://github.com/Jott2121/crucible@v1"

cd /path/to/your-repo-clone # work in a clone: crucible writes scope config crucible score . --module yourpkg/yourmodule.py --coverage 97

97% line coverage, but 25 of 71 injected defects SURVIVED this suite (46 killed, mutation score 65%).

No model is called and no API key is needed. crucible score detects your layout, writes the mutation scope, and proves a fresh test file is collectable before anything else runs (a canary probe; it refuses — exit 4 — rather than guess). If your repo already configures [tool.mutmut], drop --module and crucible grades the scope you chose instead of overwriting it.

The survivor count is plain mutation testing on your own suite. No AI is involved yet — the number that embarrasses a coverage badge is free to compute.

Then harden — the adversarial loop

crucible harden . --module yourpkg/yourmodule.py \ --tester claude-cli --critic claude-cli --runs-dir ~/.crucible-runs/yourrepo

With claude-cli, model calls run through Claude Code headless on your Claude subscription — $0 metered spend, and every run's meta.json records billing: max-plan so plan-covered shadow dollars are never mistaken for an invoice. No subscription? --tester anthropic uses the metered API via ANTHROPIC_API_KEY.

Lean invocation is the default: the subprocess runs with --tools "", collapsing Claude Code's agent loop to a single completion. On the reference run (rag_guard/guard.py), that took the harden from 439,230 to 3,641 input tokens (120.6×), with identical results — the same 25/25 surviving mutants killed (receipts 20260712T050833Z vs 20260712T171312Z). Measured on that run's receipts, not a universal constant. CRUCIBLE_LEAN=0 restores the ambient invocation.

Receipts are the product

Every run writes a receipt directory:

meta.json # models, billing (api vs max-plan), lean_isolation rung, scope commit receipt.jsonl # one line per round: tokens in/out, cost, kills, survivors, prompt hash result.json # verdict + totals

Generated tests are written into the working tree of wherever you run it — so run it on a throwaway branch; the bundled harden-tests skill (.claude/skills/harden-tests/) enforces the full ritual: local branch only, never main, PR strictly opt-in. If the canary can't prove your scope, crucible refuses instead of spending tokens.

Results

The pre-registered experiment (experiments/PROTOCOL.md) ran five subjects across three arms (one-shot, same-lineage adversarial loop, cross-lineage adversarial loop). H1 — the adversarial loop kills more mutants than one-shot generation — is supported: pooled exact McNemar p = 4.9×10⁻³², b = 105, c = 0. This replicates the direction established by MuTAP, AdverTest, and Meta's ACH (see docs/RELATED-WORK.md) in a new agentic, repo-level, Python setting — we claim the replication, not the idea. H2 — a cross-lineage critic beats a same-lineage critic on missed survivors — is not supported (p = 0.0625). An earlier run showed an enormous H2 effect; the autopsy traced it to silent output truncation rejecting one arm's rounds — an instrument artifact, not a model difference. That autopsy, and the fail-closed instrumentation built from it, is the finding. Full tables, all three pre-declared views, cost-per-kill, and the instrument-repair narrative: experiments/RESULTS.md.

Why trust this

The claims above are checkable: the experiment was pre-registered before results existed (experiments/PROTOCOL.md), the null is published at the same prominence as the positive result, the prior art is cited rather than rediscovered (docs/RELATED-WORK.md), and the tool is dogfooded — crucible's own modules run under the same mutation gate, current score and survivor dispositions in docs/MUTATION.md.

Honest limitations

Python + pytest repos only; layout heuristics target well-formed projects — a repo the canary can't validate is a refusal, not a guess.

mutmut is pinned exactly (3.6.0): the src-layout shim relies on a mutmut-internal contract.

The claude-cli provider has no mechanical truncation check (the CLI exposes no output cap); disclosed in the provider docstring.

The 120.6× lean result is one module, one apples-to-apples pair of runs. Your ratio will differ; your receipts will tell you.

How it works

Tester (writes tests) ──> mutmut (injects defects, counts kills) ^ │ named survivors └──── Critic (kills exactly those) <──┘ ... until dry or round cap

Built on oracle-gate (survivor triage, provenance, providers). MIT license.

About

Adversarial test-hardening for AI-written code: a Tester writes tests, mutation testing finds what they miss, a Critic kills the named survivors. Mechanical verdicts, mutation-kill receipts, $0 on a Claude subscription.

Topics

testing

mutation-testing

pytest

agents

test-generation

claude

mutmut

llm

ai-generated-code

Resources

Readme

License

MIT license

Uh oh!

There was an error while loading. Please reload this page.

Activity

Stars

0 stars

Watchers

0 watching

Forks

0 forks

Report repository

Releases 1

v1.0.0 — the mutation score, in your PRs

Latest

Jul 13, 2026

Packages 0

Uh oh!

There was an error while loading. Please reload this page.

Uh oh!

There was an error while loading. Please reload this page.

Contributors

Uh oh!

There was an error while loading. Please reload this page.

Languages

Python 100.0%