The order handler was accepting a negative quantity and storing it. Asked to fix that, the agent added a condition that rejects values below zero, ran the suite, reported done. The new test passed. The next invalid order still landed in the table.
The guard existed and read correctly; it simply ran after the insert instead of before it. The test never exercised that ordering because it called the guard on its own.
A correct check in the wrong place still fails
Validation has one job: stop the request before any system state changes. Once a row is written, a payment is charged, a queue message is published, or an email goes out, checking the value afterward can only react to damage that already happened. Rejecting after the fact is logging a problem, not preventing one.
The agent copied the familiar shape of a guard clause without anchoring it to where the mutation begins. Inside the handler it found the save call, then added the check a few lines below it. The condition itself was fine; the order of operations was not.
Why the green test hid it
The test called the validation helper directly with a negative quantity and asserted it returned a rejection. That proves the condition evaluates the number correctly. It does not prove the condition runs before the insert, because the test never reached the save flow at all.
This is the recurring blind spot of unit testing a guard in isolation. The guard, the mutation, and the ordering between them are three separate claims, and isolating the guard tests only the first one. A negative input that reaches the actual handler reveals whether rejection truly precedes the write.
The failure leaves no loud error
Nothing crashes. The invalid row is stored, the response may even still look accepted, and downstream systems see a quantity they were never built to model. The bug behaves like a missing check rather than a broken one, so it survives code review: reviewers scan for a condition that rejects negatives, find it, and move on.
It also escapes the happy-path suite, which mostly feeds valid numbers through the handler. The edge case that matters, a negative input reaching the real entry point, is the case nobody drove end to end.
Place rejection before every side effect and prove the ordering
When a fix adds a guard, confirm it sits at the top of the entry path, ahead of the first database write, external call, or message publish. Rejection should be the first thing that runs, before any object is built that could be handed to a persistence layer. If different writes happen in stages, each rejection must precede the stage it guards.
Verify the ordering with a test that drives the real handler with invalid input and asserts nothing was persisted: no row in the table, no call to the external service, no published message. A guard helper tested alone cannot make that claim. The question to ask of every added check is simple: when it fires, has anything already changed?
Top comments (0)