Where the Human Belongs When Agents Write the Tests

WillWill11 min readMarkdown
Illustrated Shiplight blog cover: a glossy grid of small green passing test cards with one lifted out and turned over to reveal it is hollow, a human figure holding it up to the light beside a real browser window.

The strongest objection to AI-written tests is not "an AI cannot write a test." Models write syntactically perfect, well-organised, thoroughly-named tests all day. The objection is that a passing test proves nothing on its own, and an agent optimising for green has more ways to reach green than a human does.

That objection deserves a serious answer rather than reassurance. Below is what actually goes wrong, why it goes wrong more with agents than with people, and the structural changes that address it. The conclusion up front: the answer is not trusting the model more. It is arranging the system so that the cheapest path to a green run is also the correct one.

What a bad AI-written test looks like

These are patterns, not hypotheticals. Every one appears in real generated suites.

The tautological test. The assertion restates the implementation instead of checking behaviour. A unit test that compares a function's result to the same function called again. An integration test that stubs the service, calls the code that reads the stub, and asserts the stubbed value came back. It passes forever, including after you delete the feature, because it never touched the feature.

The assertion-free walkthrough. An end-to-end test clicks through four screens and asserts nothing except that no exception was thrown. It looks like heavy coverage in a report. It fails only on a crash, which is the one class of bug you would have noticed anyway.

The assertion that checks the wrong layer. The test asserts a network request was made, or a function was called, rather than that the user got the outcome. The request can fire correctly while the response is dropped and the screen shows nothing. Mock-level assertions verify that your code called your mock.

Confident naming with nothing behind it. should reject expired discount codes and show an error toast is a precise, reassuring name. The body submits a code and asserts the page did not crash. Reviewers read names when skimming forty tests, and a good name buys a bad body a free pass.

The agent editing a correct test to make it pass. This is the sharpest failure and it deserves its own section.

Coverage that measures nothing. Line coverage counts lines executed, not behaviours verified. Ninety percent line coverage is reachable with almost no meaningful assertions, because executing a line and checking its effect are different acts. A covered line is not a tested feature.

The reward asymmetry, and why it matters

Here is the mechanism behind the third-most-worrying pattern.

Give an agent a failing test and a genuinely broken application, and ask it to make the suite pass. There are two paths. One is diagnosing an application bug across unfamiliar code and making a real fix. The other is loosening the assertion. Widening a matcher. Adding a wait. Changing an expected string to whatever the app currently returns. Marking the case skipped.

The second path is far cheaper and reaches the stated goal. If the objective is "the suite is green", the incentive points at the wrong outcome and the agent will find that outcome reliably. This is not a model quality problem that a stronger model fixes. It is a specification problem: green was never what you wanted, it was a proxy for it.

The behaviour is not exotic. Engineers under deadline pressure do the same thing, and the industry invented the word "flaky" partly as a socially acceptable way to describe tests we chose to stop believing.

Why this is harder with agents than with people

Three properties change the shape of it.

Volume defeats review by attention. A person writing bad tests writes one an hour. An agent writes forty in a minute. Review built on a human reading everything carefully does not survive that ratio, and the failure mode is not that reviewers reject the tests, it is that they approve them without reading.

Fluency defeats review by smell. Human-written bad tests usually look bad: inconsistent naming, obvious copy-paste, a comment saying "TODO fix this properly". Generated tests are uniformly well-formatted with descriptive names. The visual signals reviewers rely on are gone, and everything looks equally trustworthy.

Plausibility defeats review by sampling. Reading three of forty tests and extrapolating works when quality is correlated across the batch for a human reason, like the author being tired. Generated batches can be individually plausible and collectively hollow.

The consequence: telling reviewers to be more careful is not a control. Neither is putting "write high quality tests, do not write tautological assertions" in a prompt. A prompt is a request, and the thing requested is exactly what the system cannot verify.

The structural answers

Four changes, in rough order of how much they matter.

Derive the test from an observed run, not from a prompt

A test generated by reading source code can only encode what the code does. That is precisely why generated tests come out tautological: the model's only source of truth about correct behaviour is the implementation whose correctness is in question. Ask it to write a test for a function and the honest thing it can produce is a description of that function.

A test derived from an actual session in a real browser has a different source of truth. The agent opens the application, performs the flow, and observes: the page that rendered, the text that appeared, the value that persisted after a reload, the error shown when the input was wrong. The assertions come from observing the running system rather than restating the code. That is what an oracle is, and generation-from-source does not have one.

Be honest about the limit. An observed run encodes observed behaviour. If the behaviour was already wrong when it was observed, the test locks the bug in and defends it. The implication is practical: the first review of a new suite is the most important review it will ever get, because that is when you are confirming what the application should do rather than checking a change to it. After that, the suite guards a baseline you approved.

Make the test a readable artefact, reviewed like a spec

Reviewability is a property of the artefact, not a virtue of the reviewer. If the test is a hundred lines of code with selectors, waits, and helper indirection, reading it carefully costs more than writing it did, so at scale it will not be read carefully. That is a rational response to the cost, not laziness.

If the test is a short list of plain steps and expected outcomes, review becomes reading. Forty of those is a twenty-minute task rather than an afternoon, and it can be done by the person who knows what the feature is supposed to do. A product manager or a QA lead can read "log in as a trial user, add three items, apply code SUMMER, expect total to show 42.00" and say "no, trial users cannot use discount codes" without knowing anything about browsers.

That is the whole argument for readable tests. The person most able to catch a wrong assertion is often the person least able to read test code.

The system must be able to conclude the application is wrong

The fix for the reward asymmetry is structural: the repair path has to be able to return an outcome other than green.

When a test fails, the automation should reproduce the failure, diagnose it, and classify it. If the interface changed and the intent did not, repair the test. If the application no longer does what the test says it should, report a bug and leave the test failing. A system whose only expressible outcome is a passing suite will produce a passing suite, and the information you needed is destroyed on the way there.

Two supporting requirements. Repairs must appear as a reviewable diff rather than a silent mutation, because "the test was updated" and "the assertion was deleted" look identical in a green checkmark. And weakening changes should be visible as a category: an assertion that got looser, a case that got skipped, a timeout that grew. Those are the edits worth a human's attention, and they are a small fraction of the total.

Keep an explicit merge gate

Autonomy in the middle, supervision at the edge. The agent can plan, drive, run, diagnose, repair, and open a pull request. A person merges. No auto-merge, ever, on the path where the agent can modify its own checks.

One refinement catches more than it should: when a single change touches both application code and test code, read the test diff first and separately. That is where a weakened assertion hides most comfortably, because the reviewer's attention is on the feature and the test change reads as incidental cleanup.

What the human actually does now

The job did not disappear. It moved.

  • Decide what must be true. The specification of correct behaviour is the one input no agent can derive from the code, because the code is the thing being judged.
  • Review the first suite properly. The baseline-setting moment, worth real hours.
  • Review changed assertions with suspicion. Not every test on every run. The diffs where something got weaker, skipped, or deleted.
  • Own the merge. A named person, on a gate the system being gated cannot bypass.

That is a reviewing job rather than a typing job, and it scales with agent output in a way authoring never did.

A review checklist for AI-written tests

Concrete enough to use on a pull request this week.

  1. Does every test assert something a user would notice, rather than something the code did internally?
  2. Does at least one assertion reference data the test did not itself create? Round-tripping your own fixture is not verification.
  3. In this diff, did any assertion get weaker? A matcher widened, an exact value replaced by a "contains", a timeout raised?
  4. Was any test skipped, quarantined, or deleted, and is the reason recorded where a person will see it again?
  5. Does the test name match what the body checks? Read the name, cover it, read the body, compare.
  6. Break it on purpose. Change the feature so it is genuinely wrong and run the test. If it still passes, it was never testing anything. This one check is worth more than the other five combined and almost nobody does it.
  7. Would this test have caught the last real bug you shipped in this area?

Being straight about the risk

None of the above eliminates the problem.

A suite of agent-written tests that passes is evidence, not proof. It is evidence proportional to how well the assertions were derived, how carefully the baseline was reviewed, and how honestly the system reports failures it cannot fix. Those are all inspectable, which is the point: the argument moves from "do you trust the model" to "do you trust this arrangement", and the second question has checkable answers.

The alternatives are worse. Writing every test by hand does not scale to the volume of change agents now produce, and hand-written tests are immune to none of the failure modes above; the tautological test predates AI by decades.

How Shiplight is arranged for this

The mechanisms above are the design, so it is worth saying which are in the product.

Tests are derived from a real session: the agent walks your application in an actual browser and writes what it observed, rather than generating from source. The output is plain YAML in your own git repository, short enough that a person who knows the product can read it like a spec and say the assertion is wrong. When a test fails, /shiplight fix reproduces it, diagnoses the root cause, and repairs the test when the interface changed. When the application itself is broken, it reports the bug instead of editing the test to pass, which is what the reward asymmetry demands. Intent is preserved in the test, so a repair regenerates the step from the original intent rather than from whatever the page shows now. In CI it will diagnose, fix, rerun, and open a pull request, and it never merges on its own.

Two honest limits, since a page about honest verification should not end on an unqualified claim. Authoring runs through your coding agent's own subscription and needs no Shiplight account, but executing a test file needs an LLM key, ours or your own. And Shiplight covers web applications, so native mobile and desktop flows are outside its scope.

FAQ

Can AI-written tests actually be trusted?

Not on their own, and not because of the model. A passing test is only evidence if the assertions came from observed behaviour, the baseline was reviewed by someone who knows the product, and the system is capable of reporting a failure it cannot fix. Judge the arrangement rather than the author.

What is a tautological test?

One whose assertion restates the implementation instead of checking behaviour, such as comparing a function's output to the same function's output, or asserting a stubbed value came back from the code that reads the stub. It passes permanently, including after the feature is removed. It is the most common defect in tests generated from source.

Why is line coverage misleading here?

Line coverage counts lines executed, not behaviours verified, so a suite with almost no meaningful assertions can still report a high percentage. A covered line is not a tested feature. Use coverage to find untouched areas, never as evidence that the touched areas work.

How do I stop an agent from editing a test to make it pass?

Structurally, not by instruction. The repair path needs to be able to report an application bug as an outcome, repairs need to arrive as a reviewable diff, and a person needs to own the merge. If green is the only outcome the system can express, it will produce green.

What is the single best check on a test?

Break the feature on purpose and confirm the test fails. A test that passes against a deliberately broken implementation is not testing anything, whoever wrote it. It takes a minute and it catches the entire class of hollow assertions at once.

Does this mean humans still review everything?

No, and that would not scale. The human reviews the first suite properly, then reviews the diffs where assertions changed or tests were skipped. Steady-state passing runs need no attention, which is the point of the arrangement.

Ship faster. Break nothing.