Blog · ← All posts
Why AI Code Passes Tests but Fails Human Review
Last updated: October 2026 · 6 min read
A founder sent us his repo last month with a note we hear constantly: "All 200 tests pass. Can you just sanity-check it?" Two days later we sent back 41 findings. Every test was green. The code was, in several places, genuinely bad.
This isn't a contradiction. Tests and human reviewers are checking different things, and AI-generated code is unusually good at satisfying one while failing the other. Here's why.
Tests check behavior. Reviewers check judgment.
A test suite checks a narrow, literal thing: whether the function returns the right answer for these inputs. Models are excellent at that. A human reviewer checks something wider: whether it returns the right answer for the inputs the tests don't cover, in the codebase this will grow into, maintained by the people who will inherit it.
Those are different questions. The second one has no test.
Take a checkout discount function we reviewed in September. It passed all its tests: percentage discounts, fixed discounts, stacked discounts, expired codes. What the tests never checked was what happened when two discount codes applied to the same line item, a case the spec mentioned and the prompt forgot. The function applied both, then applied them again on retry, because the retry logic re-ran the whole calculation. A human reviewer caught it in four minutes. The 40-test suite missed it entirely, because nobody had written a test for a scenario the original prompt omitted.
Tests encode what someone already thought of. AI code reliably contains what nobody thought of.
What green tests actually prove
Tests prove the code does what the tests check, which is usually what the prompt asked for. That's it. They don't prove the code is the right shape, that it handles the edges the prompt missed, or that a new hire can understand it in March.
This matters more for AI code than human code because of a quirk we've seen repeatedly: models write tests and implementation to match each other. Ask for code plus tests in one prompt and you'll get tests that verify the implementation's actual behavior, including its bugs. The test suite becomes a mirror of the code rather than a check on it. Green means "consistent," not "correct."
We saw a billing module with 98% coverage and a rounding bug that shaved a cent off every invoice over $1,000. The tests passed because the test expectations were generated from the same flawed arithmetic. Both were written in the same prompt, in the same minute, by the same model. Nobody was lying. Everyone was consistent.
We asked the founder when he'd last read the test expectations themselves. He laughed. "I trusted the green." That's the trap in one sentence: the tests were written by the same process that wrote the bug, and the green checkmark felt like proof. It was proof of nothing except agreement.
The four things tests never check
Across our audits, the findings that tests missed fell into four buckets:
- Readability debt. A 90-line function with seven nested conditions and variables named `tmp2`. It passes. Nobody can review it safely, which means the next change to it will introduce a bug the tests also won't catch.
- Wrong-layer fixes. The model patched a symptom in the frontend that should have been fixed in the API. Tests pass, the behavior is correct, but the fix is in the wrong place and will break when the API changes.
- Missing failure modes. Happy-path tests plus a couple of obvious errors. The real world has network timeouts, partial writes, race conditions. AI-generated code rarely handles them, and the tests rarely ask about them.
- Coupling. A feature that works but reaches into three unrelated modules to do it. Tests can't see architecture. Reviewers can.
Who wrote the tests? The model did.
This is the part nobody wants to hear, because it undermines the whole safety ritual. In a growing share of the codebases we audit, the test suite was written by the same model, in the same session, from the same prompt as the implementation. Ask for "the feature plus tests" and you get a matched pair: implementation and expectations, generated minutes apart, agreeing with each other perfectly.
That's not verification. That's the model checking its own homework. Here's a small, concrete shape of it we've seen more than once:
// The implementation rounds down at a boundary the model chose
function discount(total) { return total > 100 ? total * 0.9 : total; }
// The generated test, written by the same model, in the same prompt:
test("discount applies above 100", () => {
expect(discount(101)).toBe(90.9); // matches the code, not the spec
});
The spec said "10% off orders over $100." The model implemented "over 100" as strictly greater than 100, then wrote a test asserting exactly that behavior. Green. The boundary case, an order of exactly $100, was never questioned, because the test was derived from the code, not from the requirement. Nobody independently encoded what "correct" meant. Two artifacts, one author, zero independent judgment.
This is why we read generated test files the way we read generated code: skeptically. A useful question for every AI-written test is whether it could possibly fail given the implementation it sits next to. If the answer is no, it's documentation, not a test.
The review that catches what tests miss
You don't need to read every line. A targeted review of AI-generated code takes an experienced developer about an hour per thousand lines, and it should focus on exactly the things tests can't see:
- Read the code out loud test: if you can't explain a function in one sentence, it needs to be split.
- The seams: where the code touches the database, the network, or other modules. Every seam is a place tests under-specify.
- The prompt's blind spots: re-read the original prompt and list what it didn't say. The code is guaranteed to be weakest there.
- One failure injection: pick the external dependency, imagine it failing, and check whether the code behaves sanely. Tests almost never cover this for AI-written code.
Here's where it breaks, honestly: this kind of review requires a developer who understands the domain well enough to know what the prompt didn't say. A junior dev reviewing AI code they couldn't have written will mostly rubber-stamp it. The review is only as good as the reviewer's judgment, which is exactly the thing the AI doesn't have.
Tests are necessary. They're just not sufficient.
Keep the test suite. Green CI is still the baseline. But treat "all tests pass" on AI-generated code the way you'd treat a spellcheck on a legal contract: it checked the spelling, and the spelling was never the risky part.
One more thing, for teams that want an automated answer to the mirror problem. There's a technique called mutation testing, and it's the closest thing to a mechanical check on test quality: the tool deliberately introduces small bugs into your code (flips a comparison, deletes a line, changes a constant) and checks whether your tests catch them. If a mutated bug survives the suite, your tests weren't really testing that behavior. Tools like Stryker for JavaScript and mutmut for Python do this today. It's slow, it's noisy, and on AI-generated codebases it finds the hollow tests fast: the ones that assert the implementation's behavior rather than the spec's requirements. Run it once on a generated module and you'll never trust a coverage number the same way again.
But even mutation testing only checks what the tests cover. The judgment questions, the seams, the prompt's blind spots, the failure nobody imagined, still need a human who understands the domain. That's the review. There's no shortcut past it, only tooling that makes it cheaper.