There's a quiet assumption creeping into a lot of teams right now: that AI-generated code needs less scrutiny than code a person wrote, because typing speed was supposedly the bottleneck all along. The primary bottleneck was rarely typing speed — it was establishing correctness, and that got harder to verify by eye exactly when code started getting written faster than anyone can carefully read it.

Why AI-generated code needs more scrutiny, not less

A large language model is optimized to produce code that looks right: plausible variable names, idiomatic structure, a docstring that sounds confident. That's what its training data rewards. Looking right and being right are correlated, but not the same thing, and the gap between them is exactly where the failures live: a hallucinated edge case handled with confident-but-wrong logic, an off-by-one that mirrors a thousand similar-looking examples instead of this specific requirement, an assumption about input shape that happened to hold in the training distribution and doesn't hold in your database.

Human-written code has a built-in check that's easy to take for granted: writing it forces you to think through it. Typing out a loop, a validation branch, an error path builds a mental model of what can go wrong as you go. When an AI writes the same function in two seconds, that walk-through never happens unless something else forces it. Tests are that something else. Skipping them removes one of the few structured steps where anyone is forced to reason about inputs beyond the one shown in the prompt.

There's also a volume problem: AI makes code cheap to generate and expensive to review at the same depth, and a reviewer skimming a 400-line diff catches less per line than the same reviewer reading 40 lines they watched get written. Automated tests scale with that volume; review alone doesn't.

"It compiles and the happy-path demo works" used to be a weak but real signal. With AI writing the code, that bar is now trivial to clear almost by default — which means it has stopped being a useful signal at all. Something has to replace it, and tests are the only thing that scales.

Have a different agent write the tests

The single highest-leverage practice here: the agent that writes the tests should not be the same agent, or the same context, that wrote the implementation. If one model implements a function and then writes its own tests against that same reasoning, it tends to encode its own assumptions into both halves. A bug caused by misreading the spec gets tested against the misreading, not the spec, and the test suite goes green while confirming the wrong behavior. That's not a hypothetical failure mode. It's the natural result of asking one line of reasoning to both write an answer and grade itself on it.

This is the AI-native version of "don't let someone review their own pull request," and it's arguably a sharper problem than the human version. A person reviewing their own code at least brings a second pass, a different mental state, maybe a night's sleep. A single LLM context asked to write tests immediately after writing the implementation has no such gap — it's often reasoning from the same misunderstanding twice in a row, consistently.

The fix is structural, not just a reminder to "be careful": give test-writing to a separate agent — a different session, a different context window, ideally a different prompt entirely — that receives the requirements and the public contract (function signatures, API schemas, expected inputs and outputs) but not the implementation logic. That agent writes tests against what the code is supposed to do, without ever seeing how it actually does it. When the two disagree, that disagreement is the signal, and it's a real one, because it didn't come from the same reasoning checking itself.

The strongest version may use different model families for implementation and review — Claude implements, GPT reviews the diff and writes the tests, or the reverse. Different models often approach requirements differently. That can reduce the chance that the same mistaken assumption appears in both the implementation and its tests. It's not a guarantee, though: vendors train on overlapping public sources, converge on the same common coding patterns, and can share the same blind spot despite being different models. What cross-vendor separation adds is another useful layer of independence on top of a separate session — not a substitute for actually reviewing what the tests assert.

In practice this is easy to wire into a normal workflow: one agent (or developer) implements against the spec and its public contract, a second agent writes tests against that same spec and contract independently, and CI runs the second agent's tests against the first agent's code before anything merges. Neither side sees the other's actual work until the test run itself is the referee.

Independent implementation and test agents, refereed by CI Spec + public contract Agent A — implementation implements the logic Agent B — verification designs tests from spec + contract CI: Agent B's tests run against Agent A's code ✓ pass → merge ✕ fail → blocked, back to Agent A
Both agents work from the same spec and public contract — function signatures, schemas, expected behavior — but only Agent A sees the implementation logic. The test run itself, not either agent's confidence, is what decides whether the change merges.

Coverage should be high — but risk and assertion quality matter more than the number

With AI-authored code, untested lines are exactly where the risk concentrates: nobody built a mental model of that branch by writing it, and nobody's manually confirmed it does what it claims. Push coverage high on that code, especially on risk-critical logic — payments, authentication, anything that mutates data — precisely because the usual excuse for leaving a gap, "the person who wrote this understands it well enough," is weaker than it used to be.

But coverage percentage is a blunt instrument, and it's worth being specific about which kind you're looking at. Line coverage only confirms a statement executed, not that its result was checked. Branch coverage is a better signal — it confirms both the if and the else actually ran — but neither one says anything about whether the assertions attached to that run would catch a wrong answer. Some code also genuinely resists useful unit testing — thin framework wiring, defensive branches guarding conditions that can't occur given the caller's contract, glue around a third-party SDK you don't control — and forcing coverage onto those mostly just adds tests that execute a line without asserting anything meaningful.

Mutation testing gets closer to the question that actually matters: if this line's logic were subtly wrong, would any test actually fail? A mutation-testing tool changes a > to >=, or swaps in a hardcoded 0 for a variable, then reruns the suite automatically. A test suite that survives the mutation unchanged has a coverage number but no real detection power behind it. That's the AI-specific risk in one sentence: coverage tells you a line ran; mutation testing tells you whether the tests would catch it being wrong. AI-generated code is disproportionately likely to be wrong in ways that still execute cleanly.

An agent instructed to "get coverage to 100%" will do exactly that, including the parts you didn't mean: tests that call a function and assert it didn't throw, with no check on what it actually returned. That satisfies the metric and proves almost nothing. Coverage percentage is a proxy for "this behavior has been verified," not the goal itself — review what the assertions actually check, not just the number the coverage tool reports.

Test the failure paths, not just the happy path

A model asked to write tests for a function will reliably produce a solid positive test: valid input in, expected output out. That's necessary, but it's the easy half. The examples most visible in documentation, tutorials, and accepted answers tend to demonstrate successful execution, which can make failure handling less prominent in generated implementations. A test suite of only happy-path cases will never catch that gap.

A minimal test set for almost any function should cover both sides deliberately. For something as simple as a function that validates a payment amount before a charge goes through — deliberately taking integer cents, not floating-point dollars, since binary floating point is exactly the kind of quietly-wrong behavior this whole approach exists to catch:

// Positive — valid input behaves correctly
test("accepts a valid charge amount in cents", () => {
  expect(validateAmount(4999, { minimumCents: 50 })).toEqual({ valid: true, cents: 4999 });
});

// Negative — invalid input fails correctly, not silently
test("rejects a negative amount", () => {
  expect(() => validateAmount(-500, { minimumCents: 50 })).toThrow("Amount must be positive");
});

test("rejects a non-numeric amount", () => {
  expect(() => validateAmount("free", { minimumCents: 50 })).toThrow("Amount must be a number");
});

test("rejects an amount below the configured minimum", () => {
  expect(() => validateAmount(10, { minimumCents: 50 })).toThrow("Amount below minimum charge");
});

test("rejects a non-integer cent value instead of silently rounding", () => {
  expect(() => validateAmount(49.5, { minimumCents: 50 })).toThrow("Amount must be a whole number of cents");
});

Notice what the negative tests are actually checking: not just that something goes wrong, but that it fails in the specific, correct way — the right error, not a crash somewhere unrelated, and not a silent fallback that quietly does something the caller never asked for. An AI-generated implementation is entirely capable of "handling" a negative amount by clamping it to zero instead of rejecting it — plausible-looking code, wrong behavior, and a test suite of positive cases alone would give that a passing grade forever.

The same idea extends past simple validation: boundary values (zero, empty string, empty array, the maximum allowed size plus one), malformed or unexpected types, downstream dependency failures (the database times out, the API returns a 500, the queue is full), and concurrent or out-of-order access where relevant. None of these show up if the only thing being tested is "does this work when everything goes right," which happens to be the case an LLM is generally most likely to get right without any testing at all.

Unit tests are one layer, not the whole strategy

Everything above is specifically about unit tests because that's where the independent-agent and coverage practices apply most directly. Unit tests alone don't get AI-generated code to production-ready, though. Code that touches a database, a queue, an external API, authentication, or infrastructure needs integration and contract tests that verify those boundaries actually behave the way the unit tests assumed they would. Static analysis, dependency scanning, property-based testing, and a targeted security review catch categories of failure — a vulnerable dependency, an auth bypass, an input that breaks an invariant no example-based test happened to think of — that unit tests, independent-agent or not, were never going to catch. The principle carries over unchanged: faster code generation means the automated verification around it needs to get broader, not stay fixed at whatever one layer of unit tests can cover.

Where this leaves you

None of this is a reason to slow down on using AI to write code — it's a reason to be precise about what still needs a human-designed check around it. A short version of the practice: a separate agent, potentially using a different model for additional independence, writes tests from the spec and public contract, not from the implementation; coverage stays high on risk-critical logic with a real review of what the assertions actually check, not just the percentage a tool reports; and every function gets both the positive case and a deliberate set of failure cases, because that second set is where AI-generated code is most likely to be quietly wrong. Wire the test run into CI as the actual gate, and "the AI wrote it" stops being a reason to trust code more and starts being exactly the reason it gets checked harder.

If your team has scaled up how much code AI is writing faster than your test practices have kept up, tell us where the gap is and we can help you close it — from CI setup to the testing discipline that makes AI-assisted development actually safe to move fast with.

Joseph Rounds

Founder, Lighthouse Consulting

25+ years building enterprise software at McKesson (Fortune 10), Doctor On Demand, and IntelyCare. Now helping Boston-area businesses design and build custom software, AWS infrastructure, and AI integrations that fit how they actually operate.