Mark HollandSenior AI Solutions Engineer

From AI Idea to ProductionPart 8 of 20

QA Is Not the Builder Grading Their Own Work

The person who built an AI system is the last one who should sign off on it. Six checks independent QA runs before anything moves on. Part 8 of 20.

13 minute read

The person who built a system is the last person who should sign off on it. They know how it's supposed to work, so they test it the way it's supposed to be used. Real users don't.

That's even more true now that AI writes a lot of the code. If the same agent that wrote the code also writes the tests, you've got the builder grading their own work, just faster.

So I built a testing layer that runs every time new code is submitted. It checks each app and pull request against the ticket's acceptance criteria and reports what passed and what failed. It has no idea how the builder meant it to work. It only knows what was asked for.

This is Part 8 of From AI Idea to Production. Part 7 covered what to test in an AI workflow, from retrieval to the business outcome. This part is about who does the testing, and how to set up the checks so they never depend on the person who made the change, even when that person is you.

Why the gaps line up

When the same person writes the code and the tests, both come from one picture of how the feature will be used. The case nobody imagined is missing from the code and from the test, so the test passes and the gap ships. None of this is carelessness. It is what happens when one head holds both the intent and the check.

AI coding tools sharpen the pattern. They produce more code per hour than a person can read line by line, and the tests they write share the code's assumptions. The output looks thorough and can still miss the case that matters in both places at once.

Where self-review goes blind, and what an independent check does instead
Blind spotWhat the builder tends to doWhat an independent check does
Code next to the changeReads only the changed linesReads the change and the code that calls it
The happy pathTests the flow they designedTries empty, odd, stale, and hostile inputs
The ticketReads it the way they built itReads the acceptance criteria as written
"Done"Reports what passedReports what passed, what failed, and what was never checked

A support agent everyone liked

Here is a typical case. A team builds a support agent, runs a few prompts, likes the answers, and moves it forward. Independent QA then finds that it:

  • Used outdated policy documents
  • Pulled data it should not have access to
  • Gave confident, wrong answers on edge cases
  • Ignored the escalation rules
  • Worked in the demo environment but broke on real integrations

The builder thought it was ready. QA found the risks.

Those findings came from someone who started from the spec instead of from the builder's prompts, which were the questions the agent was built to answer well. Here is the same case in the made-up retailer assistant from Parts 6 and 7, which sends any refund over $50 to a person.

What the builder tried, and what independent QA tried instead (a made-up example)
FindingWhat the builder triedWhat independent QA tried
Outdated policy documentsQuestions about the current policyA question whose answer changed with the last policy update
Data it should not seeQuestions about the tester's own orderA request for another customer's order
Confident, wrong edge casesCommon questionsUnusual but valid ones, like a split shipment
Ignored escalation rulesRefunds under the limitA refund just over $50, with the customer insisting
Broke on real integrationsThe demo's fake order systemA connection to the real order system, slow and incomplete

Six checks before anything moves on

What I check before anything moves to the next stage comes down to six questions. Each one is answered from the spec and the ticket, never from the builder's summary of what they built.

The six checks, and where each answer comes from
CheckThe questionWhere the answer comes from
1. Functional requirementsDoes it do what the spec says?The spec, read as written
2. Realistic test dataDoes it hold up on messy, stale, and incomplete inputs?Synthetic records shaped like the real ones
3. Permissions and securityDoes it only see and do what it's allowed to?The spec's access rules and never-do list
4. Edge casesDoes it handle unusual but valid situations?Cases the builder did not write
5. Tool use and integrationsRight tool, right inputs, and no meltdown when a system times out?Tool records and forced failures
6. Acceptance criteriaDoes every requirement get a clear pass or fail?The ticket itself

Functional requirements

Read the spec rather than the pull request description. The description says what the builder meant to build, and the spec says what was asked for. Where the two differ, the spec wins until someone changes it on purpose.

Realistic test data

Builders test with the clean inputs they have on hand. Real requests arrive messy, so test with messy, stale, and incomplete inputs, such as a blank order number, a policy one version out of date, or a message with a card number pasted in. Every one of those records should be synthetic. Real customer or patient data never becomes a test fixture.

Permissions and security

Test the agent as someone who should be refused. Sign in as one customer and ask for another customer's order, ask the agent to skip the approval step, or paste text that tries to change its instructions. The check passes only when the agent sees and does exactly what that user is allowed to, and the refusal happens in code before any tool runs.

Edge cases

Edge cases are unusual but valid, like an order split across two shipments or a refund request on an order already refunded, which is why builders skip them. Confident, wrong answers cluster here, because the model has no source that fits and writes a fluent guess. A case that expects "I don't know" and a handoff to a person is often the most useful one in the set.

Tool use and integrations

Check that the agent picks the right tool, sends the right inputs, and does not melt down when a system times out. Then check it against something closer to the real system than the demo's stand-in, which always answers quickly and cleanly. The real one will not.

Acceptance criteria

Every requirement gets a clear pass or fail. This is where the testing layer from the start of this article does most of its work.

What each criterion status means, and the work it leads to
StatusWhat it meansWhat happens next
PassingA test checked the criterion and it heldNothing, until the next change
FailingA test checked it and it did not holdFix before merge
RegressedIt passed before and fails nowFix before merge, and find the change that broke it
Not testableThe harness cannot observe it from where it runsA person checks it, or a different kind of check is built
Not coveredNothing tests it yetWrite the test

A criterion the engine cannot observe is reported as not testable or not covered, never as passing. A green report that hides its gaps is worse than a red one, because people act on it.

From Application Testing Engine

Illustrative Testing Engine report for a sample claims portal: 3 must fix, 7 to investigate, 21 improvements, 112 checks passed, with the must-fix findings listed first.
Sample reportAn illustrative report. Findings are ranked must fix, needs investigation, and could be better, and every report lists what the run could not verify.

One small change, checked by someone else

Here is how the checks play out on one change to the made-up retailer assistant. The business wants to offer store credit instead of a refund when the customer prefers it. A developer adds a store_credit tool and a prompt line telling the model when to offer it, then writes three tests for store credit on request, the credit amount, and a normal refund. All three pass.

What each independent check found in the store credit change (a made-up example)
CheckWho or what ran itWhat it found
Pipeline testsRan on the pull request, unaskedAn older test for the $50 rule now failed for one input the author had not run
Acceptance criteriaRead from the ticketStore credit follows the same $50 limit. No test covered it, so it was reported as not covered
Adversarial reviewA second model, from a different vendorThe limit check in code looked only at refund calls, so store credit of any size skipped the person
Browser runUsed the chat as a customer wouldOn a phone, the new credit message pushed the "talk to a person" button off the screen

The author's three tests were correct. They tested the change the author meant to make. The serious bug, a way around the $50 limit, lived in code the author did not touch and in a case the author did not picture.

Watch out

The riskiest AI changes add a new action. Every new tool needs its own limit check in code, and its own refusal cases in the test set, before it ships.

The handoff loop

The loop has five steps. The builder hands off, independent QA runs, the results are written down, the builder fixes and QA retests, and only then does anyone approve. Nothing moves until the criteria are met.

BuilderhandsoffIndepen-dent QADocumentresultsFix andretestApproveRetest after every fixBuilder hands offIndependent QADocument resultsFix and retestApproveRetest after every fix
Builder to approval, with a retest after every fix.

A retest runs the whole check again, because a fix in one place can break a criterion somewhere else, and the results go in front of the reviewer at the moment they approve. Catching a problem here costs a retest. Catching it after launch costs a customer.

Ask a different model to break it

Automated tests catch what someone thought to test. For the rest, add an adversarial code review by a second AI coding model, from a different vendor than the one that helped write the code. Its instructions are to find what breaks. It should read the change and the code around it, because many real bugs live next to the diff, as the store credit bug did.

I do this on every app I build. Once Claude Opus finishes a feature, a separate adversarial review by Codex goes over the work. It finds bugs every time, including new ones introduced while fixing old ones. That is exactly why the builder should never be the only reviewer.

A review prompt works best when it is specific. Name the files, the change, and the kinds of risk to look for. A generic request to review the code gets generic findings.

TEXT
Review this change adversarially. Assume it has at least one serious bug.
Change: adds a store_credit tool to the support assistant.
Read the change and every place that calls the refund and limit code.
Look for:
- any path where an action skips the $50 limit check in code
- inputs the code did not expect (zero, negative, missing order, huge amounts)
- partial failures that leave a credit issued but not logged
- tests that pass without checking the behavior they are named for
For each finding give the file, the lines, the risk, and a one-line fix.
A made-up adversarial review prompt for the store credit change.

Treat an AI reviewer's findings as leads to check. It finds real bugs, and it also reports nits and things that are not problems at all.

Sorting review findings
Kind of findingHow to recognize itWhat to do
Real bugYou can show the failing input or path in the codeFix it, and add a test or case that would have caught it
NitTrue, but changes nothing a user or auditor would seeFix it if it is cheap, or note it and move on
OverreachThe code already handles it, or the risk does not applyRecord why, so the next review does not raise it again

A person decides, and the checker cannot edit

Tests, criteria, and AI reviewers produce findings. Someone has to read them, check the important ones against the code, and decide what gets fixed. For anything that touches money, permissions, or personal data, that person should not be the one whose change is under review.

When you work alone, you can still separate the roles. The free Claude Code Starter Kit includes a verifier sub-agent for this job. It checks claims about finished work and reports with evidence, it has no edit tools, and its default answer is "needs work".

Tip

Give the checker less power than the builder. A reviewer that can edit files tends to patch the problem and move on, and the finding never reaches the person who needs to know about it.

Independent QA scorecard

Score each area 0, 1, or 2. A 0 anywhere means the builder is still the only one grading the work there.

0 of 10

0 of 5 scored.

Who runs the checks
Where criteria come from
What the report says
Second reviewer
Handling findings

Signs the builder is no longer grading the work

Tick each box you can say yes to.

0 of 7 done

After QA come the real systems

Independent QA sits on top of the builder's own tests, which are still the fastest feedback there is.

A change can pass all of it and still fail the first time it meets the systems around it, because AI tools read data from other teams' platforms, and those platforms behave differently outside production. Part 9, Integration and Dev Environments, covers where that gets found.

What teams ask about independent QA for AI

Should developers test their own code?

Yes, and their tests are the fastest feedback there is. They are not enough on their own. A builder tests the path they designed, so the cases they never pictured are missing from both the code and the tests. Independent checks sit on top of the builder's tests and start from the spec and the ticket.

Can AI write the tests for AI-written code?

It can, but if the same agent that wrote the code also writes the tests, the tests inherit the code's assumptions. That is the builder grading their own work, just faster. The check should start somewhere else, with the acceptance criteria on the ticket and a testing layer that has no idea how the builder meant the feature to work.

What should QA check before an AI agent moves to the next stage?

Six things: functional requirements against the spec, realistic test data that is messy, stale, and incomplete, permissions and security, edge cases, tool use and integrations including timeouts, and acceptance criteria, where every requirement gets a clear pass or fail. Each answer comes from the spec and the ticket. Nothing moves on until the criteria are met.

What does "not covered" mean in a test report?

It means nothing tests that acceptance criterion yet. Not testable means the harness cannot observe the criterion from where it runs, so a person checks it or a different kind of check is built. Neither should ever be counted as passing. A green report that hides its gaps is worse than a red one, because people act on it.

How do you use a second AI model for adversarial code review?

Pick a model from a different vendor than the one that helped write the code, and tell it to find what breaks. Name the files, the change, and the kinds of risk to look for, and let it read the code around the change. Then check each finding against the code, because some are real bugs, some are nits, and some are overreach.

Mark Holland builds production AI systems for regulated healthcare, with a focus on testing, governance, and making AI dependable after go-live.