Tests First, Then “Implement All Plans”: Designing Services an Agent Can Verify
Part 6 of 9 in the series on agentic software development. Part 1, The Agent Writes the Code has the two settings, the numbers, and the ten sentences the series works through.
In the event-driven service this series draws on, the requirements came from outside and the decision tables existed before any code did. That gives you an oracle. It does not yet give you a service an agent can verify. For that, the business logic has to run in a test without Kafka, without Postgres and without the network, and the tests have to exist before the implementation, red for a reason you can prove. Both are design decisions, and both are made before the first line of business logic.
Transport is an adapter at the edge
I always design a service so that its function works and is testable without the external connections. For an event-driven service that means: the business logic must not care whether an event arrives via Kafka, a message bus or REST, nor how the result leaves. Transport is an adapter at the edge.
┌─────────────────────────────────────────┐
Kafka ──► │ adapter │ business logic (pure + │ adapter │ ──► Kafka
REST ──► │ │ ports: persistence, clock, │ │ ──► REST
│ │ outbound, log) │ │
└─────────────────────────────────────────┘
The business logic talks only to ports: one contract each for persistence, outbound messages, the clock, logging and external data. In tests, the ports are recorders and fakes; in production they are Kafka, Postgres and HTTP. The logic in the middle cannot tell the difference, and that is the whole point.
A recorder is an implementation of a port that keeps what it was given, so that a test can assert on it: which outbound messages were published, in which order, with which payload. A fake clock returns whatever time the case says it is. Neither knows anything about a broker. Persistence is a port like the others, and until the stage that checks the real database, it is satisfied by something in memory.
Why this matters more with an agent
With a human developer, the argument for this design, ports and adapters, test-driven development, is the familiar one: fast tests, clean boundaries. With an agent, three things weigh more.
- All tests can run immediately. No broker, no database, no network is needed to verify the business logic. The feedback loop is seconds, not minutes, and it is available in every session from the first day. An agent that has to wait for infrastructure runs the suite less often, and an agent that runs the suite less often reasons more and checks less.
- The agent can check itself constantly. This is the key point. An agent is very good when it can verify its own work after every step. It is mediocre when it has to guess whether something worked. A design in which every business rule can be exercised in a unit test turns most of the agent’s work into the first kind.
- Adapters stay thin. They translate, they do not decide. Rules at the edge are rules nobody tests; if the adapters have no room for decisions, untested rules have nowhere to live.
Five stages, each with a prohibition
Implementation of the event-driven service ran in five stages. What keeps the layers honest is not what each stage does but what it must not do:
| Stage | Subject | Prohibited |
|---|---|---|
| 1 | Pure functions | no clock, no database, no messages |
| 2 | Business logic against ports | no real infrastructure; injected clock; recorders instead of adapters |
| 3 | Persistence | no business rules; only that what is meant gets written and read |
| 4 | Edges | configuration, startup, shutdown, adapters |
| 5 | End to end | the built bundle, with a smoke test |
The order is also the order of trust. Nothing in stage 2 rests on anything that was not proven in stage 1, and the adapters of stage 4 are written when the logic they wrap already passes every business test. Stage 5 runs the built bundle, not the source: a smoke test against the artefact that would actually be deployed, because a suite that is green against the sources says nothing about what the build step did to them.
Stage 3 deserves a word of its own. Persistence tests are the ones most likely to test an imitation: a mock that answers what the test wants to hear. Here an in-process Postgres engine replays the real DDL script, so the test checks the schema the service will run against, not a stand-in for it. And the prohibition on business rules in that stage keeps the rules where the tables can reach them.
The red chain
Clear interfaces bring a big advantage: you can write all the tests first and run them against an empty function body. The expected result is that every test fails with “not implemented”. In the service, the sequence was this.
- The contract. All types, all error classes, all ports: 18 port methods and four pure functions. Each throws “not implemented”, with the same wording from one constant. No behaviour, only shape. Type checking is green. One constant rather than 22 strings typed by hand, because the message is what everything downstream filters on; a second wording would look like a real failure among the expected ones.
- The oracle. A loader that reads the generated case folders, and one place
that translates the decision table’s short
codes (
INS>OPEN,UPD>CLERK,IGN,ERR) into assertions. That translation is tested itself. Everything else in the test code passes values through without interpreting them. The alternative, each test reading the codes on its own, would be hundreds of places to be wrong and not one of them tested. - The red chain. All tests written before any business logic. First 112 red and 15 green, the green ones being the tests of the oracle itself; then 245 red, all with the same message.
Why the identical message matters: it is the only moment you can prove the tests assert something. If all 245 fail with “not implemented”, the suite so far tests nothing but itself, which is exactly what it should do at this point. A test that fails for any other reason, a missing fixture, a wrong import, a code the oracle does not know, is a broken test, and found now it costs minutes. Found later, it hides among real failures. Check the message by machine, not by eye; the test runner can emit its results as structured data, and 245 lines are more than anyone reads carefully.
“Implement the plans. Use the tests to check the result.”
When all plans exist and the red chain stands, the instruction to the agent becomes simple:
Implement the plans. Use the tests to check the result.
This is where the agent is at its best. It has a precise, machine-checkable target, a fast feedback loop, and no room for interpretation of what “done” means. Its loop is short: run, read the red list, implement, run again. Every step either turns tests green or it does not; there is nothing to argue about and nothing to summarise optimistically. The plans exist because the designs exist, and the tests exist because the tables exist; the agent adds the third layer to two that are already fixed.
At the end of stage 2 of the event-driven service: 280 tests green, without a single expectation changed. Eight times something had to be adjusted, and every time it was the setup of a case, never the expected result. Each of the eight corrections is documented with a reason, in the table or in the design.
Zero changed expectations is not luck. It rests on one rule, and the rule carries the whole approach:
When a test is red, change the code. If you want to change the expectation, change the table first — and write down why.
Without this rule, an agent rewrites the expectation until the test passes, and so does a human under time pressure. The rule does not forbid changing expectations; tables are wrong sometimes. It forbids changing them in the test. The table is the source, the test is derived from it, and a change with a written reason in the source is visible to the next reader. A change in the test is visible to nobody. The eight setup corrections are the rule in practice: each is a change in a place someone reads, with the reason next to it. Part 8 has the incidents that made the rule necessary.
Counter-probes: break every safeguard once on purpose
A green test proves nothing if it would also be green when the thing does not work. So for every safeguard, we removed it deliberately and counted which tests turned red. A probe is cheap: take one thing out, run the suite, count, put it back. What it buys is the difference between a suite that is green and a suite that is known to be able to go red.
| Intervention | Expected | Measured |
|---|---|---|
| typo in a model column | model gate fires | 2 red |
| a database constraint removed | constraint is checked | 4 red |
| responsibility derivation disabled | assignment is effective | 5 of 8 red |
| leader row removed | only one instance decides | red |
| old revocation logic restored | new path is effective | 1 red |
| local time instead of fixed zone | zone is pinned | exactly 3 red |
| empty filter list read as empty set | empty means “no filter” | 13 red |
Two lessons came out of this. First, expect a number, not “something goes red”. “Some tests fail” is true of almost any intervention and therefore worth almost nothing; “exactly 3 red” is a claim that can be wrong, in both directions. Fewer red than expected means the probe missed or the safeguard does not reach as far as the design says; more red than expected means a dependency the design does not mention. Second, place the probe where the test actually reads. A probe at the wrong place stays green and makes a good safeguard look useless; you then spend time on a safeguard that was fine and none on the probe that was not.
This part comes down to three rules. Design the service so its function runs without its infrastructure; transport is an adapter, and adapters do not decide. Write all tests first and prove they are red for the right reason, with one message from one constant. And when a test is red, change the code; an expectation changes only through its source, with the reason written down. With those three in place, “implement the plans, use the tests” is an instruction an agent can execute well, and one whose result you can check.
Previous: Part 5, A Table Cannot Stay Silent. Next: Part 7, Working Through 1,200 Plans.