When I started using AI agents more actively, the number of automated checks in my projects grew quickly. At first, this looked like an obvious improvement: it is easier to give an agent a reproducible contract than to keep explaining what must not break.
In a DocOps project, we gradually built a single verification path around Make targets, CI, and contract tests. If an important property of the process could be checked automatically, I preferred to encode it in a test.
At some point, the tests stopped checking the system contract and started checking details of its current implementation.
LLMs accelerated that transition.
It started with a useful contract
The requirements were simple.
For example, a particular check had to run through one canonical Make target. The target had to exist, actually execute the required command, and be used both locally and in CI. There should not be another path that performed the same check differently.
Contract tests are a good fit for this.
They protect an engineering agreement rather than a specific line in a Makefile:
there is one public way to run the check
→ local execution uses it
→ CI uses it
→ structural changes must not create a bypass
The problem began when we tried to prove this contract too precisely.
The test gradually turned into a command parser
After each review, another hypothetical way to bypass the check appeared.
What if the required name only appears inside echo?
What if the executable is referenced by its full path?
What if the command is invoked through xargs?
What if Docker receives a different --entrypoint?
What if two commands share the same prefix?
What about different quoting styles?
Each question looked reasonable on its own. One agent found another edge case, another confirmed that the current test missed it. Then the test gained more parsing logic and a few more artificial mutations.
Eventually, the tests contained their own model of shell and Docker command semantics. They parsed arguments, distinguished actual execution from plain data, recognized different ways of invoking an executable, and handled syntactically different but equivalent commands.
The original requirement had not changed:
the required Make target must exist and run the required check.
To verify a few lines of a Makefile, we had effectively started writing a small shell command interpreter.
That was the point where it made sense to ask what we were actually protecting.
Fragility can look like reliability
A brittle test is usually one that fails after a harmless refactoring.
That was exactly what we had, except the test suite looked more reliable from the outside.
Changing quoting could require updating the test. An equivalent way to invoke the same command created another parser case. Another valid Docker invocation meant teaching the test to understand that form too.
The observable behavior had not changed.
We were not making the contract stricter. We were making the description of one implementation stricter.
The more detailed that model became, the more tightly the tests coupled the Makefile to its current shape. Changes that should have been allowed became more expensive.
LLMs were not the cause, but they were an accelerator
The same thing can happen without AI.
LLMs simply make each additional layer of complexity extremely cheap.
A reviewing agent can invent five more ways to bypass the test in seconds. The implementation agent can support them just as quickly. The next review finds another set.
The loop looks like this:
find a theoretical bypass
→ make the test stricter
→ add more custom logic
→ find a bypass in that logic
→ make the test stricter again
Each iteration looks like an improvement.
There are more tests, more scenarios, and a more convincing agent report.
Meanwhile, the question “what property of the system are we protecting?” starts to disappear.
An agent is very good at optimizing for a local task. If the task is “make this test harder to bypass,” it can keep strengthening the test long after doing so has stopped being a good engineering decision.
We deleted most of the clever checking
The solution was not a better parser.
We went back to the original contract.
Make already knows how to read a Makefile and expand a target. So part of our custom logic could be replaced with Make itself:
make --dry-run --silent <target>
If the target does not exist or cannot be expanded, Make can tell us. We do not need to partially reproduce its behavior inside a test.
We kept checks for properties that actually mattered: public targets, the canonical path used locally and in CI, required dependencies, and real bypasses.
We removed checks for syntactic variations that did not change the contract. Along with them went helper markers, some parsing logic, and mutations that mainly existed to test the parser itself.
There were fewer tests.
It became much clearer what each one was protecting.
A contract test should not know more than it needs to
That experience changed how I think about contract tests.
A good contract test does not describe the system implementation in maximum detail. It does the opposite: it leaves the implementation free to change.
If a command is written one way today and another way tomorrow, the test should fail only if an important property of the system changed with it.
I now use a simple question:
If the implementation changes while the observable behavior stays exactly the same, should this test fail?
If the answer is yes, there should be a specific reason why that implementation detail is part of the contract.
Sometimes it is.
For example, CI may genuinely need to call one canonical Make target because otherwise local and server-side verification can drift apart.
The exact quoting inside the command usually is not such a contract.
Mutation testing needs a boundary too
The same experience changed how I think about mutation testing.
The number of artificial mutations that make a test fail does not by itself say much about the quality of that test.
A useful mutation should represent a real contract violation.
Removing a required command is useful.
Replacing the required target with another one is useful.
Creating a real bypass around the required check is useful.
Exhaustively trying syntactically different forms of the same shell command is not useful if syntax is not part of the contract.
Otherwise, the test suite starts protecting itself.
I now constrain the agent’s tests too
I used to focus mainly on the scope of the implementation when giving work to an AI agent: do not refactor unrelated code, do not change neighboring components, add a regression test, run the required checks.
That is not enough.
A test can exceed the scope of a task too.
Now I care not only about whether a test catches a bug. I also want to know why that bug belongs to the contract and whether the same property can be checked at a more stable boundary.
This matters especially during independent review by another agent.
A prompt like:
find a way to bypass this test
can easily start an endless cat-and-mouse game.
A better set of questions is:
what property is this test supposed to protect
→ does it actually protect that property
→ does it check anything beyond that property
→ can the same property be verified through observable behavior
That kind of review evaluates the quality of the contract rather than the creativity of the test author.
Overengineering can be automated too
One lesson I took from working with LLMs was simple: if a requirement can be turned into a reproducible check, that is usually more reliable than leaving it as a textual instruction to an agent.
I still believe that.
But there is a second half to the rule.
A test is also an engineering decision. It can be overengineered, coupled too tightly to an implementation, and eventually maintained for its own sake.
AI agents make writing tests cheap. They do not make deciding what is worth testing cheap.
That is why the number of automated checks is no longer a reliability metric for me on its own.
A good test should be strict enough to fail when the contract is broken and loose enough to allow everything else to change.
In this DocOps project, I had to cross that boundary first — and then delete part of the test suite — before I could see it.
