The Bug That Only Appears When You Change Your Mind
A step discount came out duplicated — but only after deleting a promotion and a product and rebuilding the cart with a different bundle. The forward path was clean every time.
Add the bundle. Apply the step discount. Submit. Clean, every time, on every run.
Then someone changed their mind mid-cart, and the discount came out twice.
The Path That Broke It
The reproduction is not one action. It is a sequence of five.
Add a bundle and a step discount to the cart. Delete the promotion. Delete the product. Add a different bundle instead. Submit.
The Order Applied Promotion comes out with the step discount duplicated.
Do the same thing forwards — add, configure, submit — and there is nothing wrong. Every case in the suite followed that shape, and every case passed, correctly.
Why the Straight Path Could Never Find It
A cart is mutable state. That sentence sounds obvious and it is the whole defect.
The forward path exercises one code path: build up, then commit. The delete-and-swap path exercises a different one: tear down, then build up again on top of whatever the teardown left behind. The duplicate appears because the deleted discount's state was not fully removed before the cart was rebuilt with a different bundle — so the rebuild added a second copy on top of a residue that should not have been there.
Nobody enumerated that transition, and not out of carelessness. It happens because "it works when you build the order normally" is a true statement, and a true statement is a very comfortable thing to sign off against.
The bug only exists when the agent changes their mind. Which, in a telesales cart, is roughly what the job consists of.
What It Was Not
It was not a discount calculation problem. The discount computes correctly on the forward path and on the rebuilt cart — it is simply applied twice.
It was not a race or a timing artefact either. The sequence is deterministic. Run those five steps and you get the duplicate; run four of them and you do not.
The Same Blind Spot, From the Other Direction
In a regression cycle a few weeks earlier, a Highest-priority defect landed with the opposite symptom.
Deleting a Wi-Fi add-on from a Modify order failed outright. The error said the item cannot have less than 1 quantity.
The validation rule behind it is not wrong. It was written for the update path, where setting an active line to zero is exactly the thing you want to block. It was written by someone who was thinking about the update path, because that was the operation in front of them.
It fires on delete as well. And a guard that cannot tell "set this to zero" from "remove this entirely" turns a data-quality rule into a trap — the add-on becomes impossible to remove, permanently, for every customer who asks.
Both defects live in the same blind spot. One is state that survives a removal. The other is a rule that blocks a removal. Neither is reachable from the path everyone tests.
What I Now Write Down
Two habits came out of this pair, and both cost one extra row in a test case.
- For anything a user can add, test removing it and adding it again. The forward path proves the feature works; only the undo path proves the state resets. Where a swap is realistic — remove one thing, put a different thing in its place — that is a third case, and it is the one that catches residue, because the rebuild lands on different foundations than the original build.
- For every validation rule, list the operations it must NOT fire on. Testers reliably verify that an invalid value is rejected and almost never verify that a legitimate exit is still permitted. Delete, cancel, remove, downgrade, withdraw — walk the rule against each of them and note which ones nobody considered when it was written.
That second one has a name worth using in a refinement session: validation context. A rule is not just a condition, it is a condition plus the set of operations it applies to — and the second half lives in the head of whoever built it, for the one operation they had in mind.
The Question That Generates Both Cases
When a feature gets demoed and everyone agrees it works, there is a single question that reopens it usefully.
What happens if the user undoes this?
Not "what if they enter something invalid." Undo is not an error case — it is normal, intentional, competent behaviour by someone doing their job well. It just happens to run through code that nobody built as carefully, because nobody was picturing it.
In your last sign-off, did anything prove the state resets — or only that it builds?