dbit.one© 2026
000
booting_
Loading experience0%
dbit.one

A test that has never failed has never tested anything

Short version: an agent fixes a bug, writes a test next to it, and reports that the tests pass. Nobody checks whether that test was ever red. Put the old code back and run it: if it still passes, it did not test the fix. I built a tool that does this mechanically, ran it over 200 merged pull requests written by coding agents, and found a bug in my own tool while checking the result.

13 min read

In short

  • A test written next to a fix must fail without the fix. If it passes, it is not evidence for anything.
  • Across 17 pull requests where the check could actually run, 35 of 140 added tests (25%) passed against the code from before the change.
  • That number is concentrated: the median pull request had none, and one repository supplied 14 of the 35. It is a floor from a small sample, not an industry figure.
  • Asking another model to check does not work — no judge configuration in the published work exceeds AUROC 0.65, because judges read the confident closing tone.
  • Verifying the result by hand found a defect in the measuring tool that made it accuse good tests. The corrected number is lower than the first one I got.

The test that shipped with the fix

An agent closed a ticket about duplicate sessions. It found the bug, fixed it, added a test, and reported: fixed the de-duplication bug, added a test for it, suite passes. All three sentences were true in the way that matters least.

js
test('de-duplicates the list', () => {
  const out = dedupe([{ id: 'a' }, { id: 'a' }]);
  assert.ok(Array.isArray(out));
});
The fix was real. This is the test that came with it.

That test passes against the broken code. It passed before the fix, it passes after, and it will keep passing on the day someone deletes the fix. It is green, it sits in the diff next to the change, and it is evidence of nothing.

I have started calling this a green lie: a suite that is green for a reason unrelated to the thing it is supposed to be watching. It is not new — every codebase has some. What changed is the rate at which it is produced, and who is left to notice.

What other people have measured

These numbers are not mine. They agree with each other, which is what makes them worth repeating.

Finding
Source
75.8% of failing coding-agent trajectories in AppWorld still reported success; 45–48% in single-control tau2-bench domains
Advani, June 2026
No judge configuration across 5 models and 5 prompt strategies exceeded AUROC 0.65; the same judges reached 0.54 on AppWorld
same paper
Of 4,882 agent-generated pull requests, those changing code and tests improved coverage in 35.9% of cases in Java, 22.5% in Python
Dipongkor et al., July 2026
The most common complaint about AI among 49,000 developers: "almost right, but not quite" (45%); 66% spend more time fixing it
Stack Overflow 2025

The paper on false success gives the reason the obvious remedy fails: judges read the confident closing tone rather than the state of the machine. "Done! All tests pass." is written in the same voice whether or not it is true.

The question a tone of voice cannot answer

The whole idea is old enough to be embarrassing.

The check
  1. 01

    Put the source back

    Check out the commit the change was built on, in a throwaway worktree. The tests stay as they were written; only the source goes back.

  2. 02

    Run the new test there

    One test, on its own, by name. Read the exit code and nothing else.

  3. 03

    It must fail

    A test written for this change has to be red without it. If it passes, it did not test the change.

That is red-green-refactor. What is new is that nobody is watching the red any more, because the thing writing the test is also the thing reporting on it. The red used to be enforced by a human having to sit through it. A test that is green for the wrong reason is a cousin of one that is red for the wrong reason: both are a suite telling you something about itself rather than about the code.

So I wrote something that sits through it: alibi. No model is asked anything, there is no network call in the repository, and it gives the same answer every time.

Two decisions that were not obvious

Exit codes, never parsed output. Every runner prints results, and every one prints them differently, changes the format between versions, and changes it again when a reporter is configured. A parser that is 98% right produces a false accusation every fiftieth run, and one false accusation costs more trust than fifty correct ones earn. So each test is run on its own and only the exit code is read. It is slower, and it is the difference between "this test passed against the old source" and "this text looked like a pass".

A third verdict, because two would be a lie. A new test for a module that did not exist yet cannot import it: the runner exits non-zero without ever evaluating an assertion. Calling that an alibi would hand one to every test written against new code, which is most of them. Calling it a failure to prove would accuse a perfectly good test. So it gets its own name — provisional — and proves nothing on purpose.

What 200 agent pull requests said

A tool is a claim about the world, so I pointed it at the world. I collected 200 merged pull requests written by five coding agents — Devin, Copilot, Claude, Codegen, OpenHands — across 138 repositories and four languages, keeping only ones that changed both source and tests, and at most two per repository so no single project could dominate.

For each: check the repository out at the base commit, install what the suite needs, confirm the suite is green there, copy the pull request’s test files over the old source, and run each new test on its own.

Count
Pull requests attempted
200
Measured
17
Dropped — suite could not start, or was already red at base, or needed a database
183
Added tests that got a definitive answer
140
Passed against the code from before the change
35 (25.0%)
Could not run at base at all (provisional)
19
Existing tests deleted or switched off in these diffs
4

The honest summary is narrower than the headline number: in 8 of 17 pull requests, at least one added test would have passed without the change it shipped with. That is the part I would defend.

The tool was wrong, and checking by hand is what found it

My first run gave 34.9%. One pull request accounted for 32 of the 67 findings — half of them — so before writing anything down I went and looked at it.

Every one of those tests imported a module that the base commit did not have. They could not have passed there honestly. They should have come back as provisional, and instead they came back as accusations.

The cause: pip install -e . writes an absolute path to the original working tree into site-packages. I had put the worktree first on PYTHONPATH, which reorders the import search — but reordering cannot stop it. A module the base commit never had is not in the worktree at all, so Python keeps walking and finds the new one through the editable install. The test then passed against code that was supposed to be reverted.

The fix is to ask instead of assume: import the changed package inside the worktree and print where it came from. A path outside the worktree means the revert did not take, and those tests are reported as unanswered, naming the module and the path it leaked to. One pull request went from 33 accusations to 39 honest non-answers, and the headline went from 38% to 25%.

I am telling you this because it is the part that decides whether the rest is worth anything. A number that survives its author trying to break it is worth more than a bigger number that did not.

Six ways to buy a green suite

The repository ships a museum of them. Each is a real repository that gets built and checked in the test suite, so an exhibit that stops being caught breaks the build.

  • The new test passes against the old source — it tested nothing.
  • The failing test was deleted — green because there is less of it.
  • The failing test was switched off with one word.
  • The test asserts nothing at all — it can only fail by crashing.
  • The test mocks the thing that changed — a photo of a bridge holds no weight.
  • The assertion was softened until it agreed — it checked contents, now it checks length.

A seventh that the tool does not catch is the most useful thing anyone could send me.

What it will not tell you

  • It cannot prove a change is correct. It finds tests that are not evidence for one, which is a much smaller claim. An empty report means nothing was caught.
  • It says nothing about tests the diff did not touch — whatever alibi they had, they had before.
  • Rust unit tests live in the file they test, so reverting the source would revert the test with it. Those are reported as unexamined rather than guessed at.
  • One process per test. On a suite with heavy global setup this is slow, and there is deliberately no fast mode that trusts parsed output.
  • A characterization test — one written to lock in behaviour that already exists — has no alibi by definition. Mark it, and it stays in the report without counting against you. There is no way to silence a finding invisibly.

Running it

bash
npx skills add BOTIROFF-D/alibi     # your agent runs the check before it says done
npm install -g @botiroff/alibi      # for your terminal and CI

The skill needs nothing installed: it walks the agent through the check with plain git and the project’s own test command, so it works in a repository that has never seen node. MIT, zero runtime dependencies, works with vitest, jest, mocha, node --test, pytest, go test, cargo test, rspec and phpunit — or any shell command you give it.

The corpus harness is in the repository under experiments/corpus, together with the collected pull requests and the raw results, so the numbers above can be checked without re-running anything.

Frequently asked questions

Is this just mutation testing?

It is adjacent. Mutation testing asks whether the suite would catch an injected bug, and costs hundreds of runs to answer. This asks whether it caught the bug that was actually there: your change is the mutant. One run per new test, no operators to argue about. A mutation pass over the changed lines is included for the second question, and it is off by default.

Can a test legitimately pass before the fix?

Yes — a characterization test locks in behaviour that already exists and has no alibi by definition. Mark it with a comment and it stays in the report, labelled, without counting against the verdict. The exception being visible is the reason it is safe to have.

Why not ask a model to review the tests instead?

Because it has been measured. No configuration across five judge models and five prompt strategies exceeded AUROC 0.65, and the same judges reached 0.54 on AppWorld. The judge reads the confident closing tone. An exit code does not have one.

Does 25% mean a quarter of agent tests are worthless?

No. It means that in the seventeen pull requests where this could be checked mechanically, a quarter of the added tests passed against the code from before the change. The median pull request had none, and one repository supplied 14 of the 35. Treat it as a floor from a narrow sample.

Is one process per test not unusably slow?

On a suite with heavy global setup, sometimes. The tests examined are the ones the diff touched, not all of them, and the whole-suite run can be switched off. There will not be a fast mode that trusts parsed output, because parsed output is exactly what the tool refuses to trust.

Sources

Every claim here can be checked: below are the primary sources, not a retelling.

  1. 01From Confident Closing to Silent Failure: Characterizing False Success in LLM AgentsThe 75.8% figure and the judge AUROC results
  2. 02Test Coverage Analysis of Agentic Pull Requests4,882 agent pull requests; coverage improved in 35.9% (Java) and 22.5% (Python)
  3. 03Stack Overflow Developer Survey 2025"Almost right, but not quite" — 45% of 49,000 respondents
  4. 04alibi — the tool and the corpus harnessMIT; experiments/corpus holds the collected pull requests and the raw results
Author
Doniyor Botirov
Founder of dbit.one · author of this piece

This piece is signed by name, unlike the rest: there is no client behind it. The code it discusses is fully open and published under the same name — you can read it, run it and check the article’s claims instead of taking any of them on trust.

Related services

Read next

Questions about your project?

Describe your task — within 24 hours we’ll come back with an estimate, timeline and plan.

[email protected]