The button was greyed out. That proved nothing.

No. 08

A test asserted that our delete-account button was disabled, and it passed while the code behind the button would happily delete an account on an empty box. Here is the shape of that mistake, and the nine mutations we used to find the rest of them.

Last time I wrote about checks that return an honest green about something adjacent to the thing you cared about. This one is the same family, but it is the most expensive member of it, because the check in question guards an irreversible action.

Spoolbound can create an account, so it has to be able to delete one from inside the app. That is a store requirement, and our privacy policy promises it in plain words: you confirm in writing, you do not email anyone, there is no waiting period. Both halves of that sentence are claims about a binary, made in a document anyone can read.

So the question is not "did we build a delete button". It is how do you know the guard on it is a guard.

The mistake, in one line

The destructive button is disabled until you type the confirmation word. The obvious test asserts exactly that: build the panel, look at the button, assert it is disabled.

That test passes on a panel that will delete your account the moment anything presses the button.

disabled suppresses the press signal. So asserting it tells you about the styling of the gate, not the gate. Any edit that rebuilds the screen without re-syncing the button, or wires a second control to the same handler, or flips the flag for a "busy" state, removes the protection without touching a line that looks like protection.

The fix is to assert both halves: that the button refuses, and that the function behind the button refuses when it is called directly with the field empty.

Nine mutations

We do not trust a gate here until we have watched it go red on a real violation. So every guard in that file was broken deliberately, one at a time, and the failures recorded.

M2 is the one the whole file exists for. Delete the confirmation check from inside the handler, but leave the greying-out alone. Six assertions failed — and "the destructive button is present and disabled" stayed green. That is the entire lesson in one line: the old test would have signed off a panel whose handler deletes an account on an empty box.

M4 is the one that earned the mutation pass its keep. We broke the code that decides whether the server call succeeded, so a failed deletion would be reported to the player as a success. First run: everything passed. Not because the panel was fine, but because a second, unrelated guard further down caught the contradiction and returned before any damage. The file was proving the belt and saying nothing whatsoever about the braces. We closed it by asserting what the mutated panel actually says to the player, which is the thing a person would have noticed and the test had not been looking at.

M7 passed, and that was correct. We reordered the tabs in the settings panel so Account was second, expecting the "deletion is two taps" assertion to fail. It did not, because the panel explicitly opens on the Account tab regardless of the order the strip was built in. Nothing should have gone red, and nothing did.

That one is worth dwelling on, because the mutation log had previously recorded this line as producing two failures — a description of an edit nobody actually applied. A mutation log that mis-describes its own edit is the same bug as the test it is supposed to validate: a record that looks like evidence and is not. The real "deletion becomes three taps" mutation is a different one line, and it fails properly.

M9 was the subtle one. The test reads the confirmation word from the same constant the panel does, rather than hard-coding it — deliberately, so re-wording the prompt does not break the test. Which raises the obvious worry: empty that constant, and do you disarm the gate and the test at the same time? You do not, and the reason is that one scenario checks the button's state before typing anything at all. The check that saves you is the one that makes no assumptions about the input.

Two rules that came out of it

Drive the real thing, not a convenient copy. The test opens the actual app shell and taps through it, rather than constructing the panel directly. A panel that works when a test builds it and cannot be reached from the running app is not a feature; it is a screenshot. That distinction is not academic — it is a state this project has been in.

Check refusals more than one way. Every "it did not delete" assertion checks three independent things: no request was sent, the account still exists, and the session is still signed in. Those can only disagree if something is lying, and the point of three is to notice when they do.

And one small courtesy, because a test that costs you something is a test people switch off: the whole thing runs against a fake server with no network in it, at an address under a domain guaranteed never to resolve, and it snapshots the developer's own session and puts it back before reporting. Signing yourself out of your own game is a rude way to discover a test touched something real.

The thing under all of it

A promise in a published document is only worth what the binary does. The gap between them is not visible from either side on its own — you can read the policy all day and learn nothing about the code, and read the code all day and never notice it contradicts a sentence somebody published.

A test that presses the real button is the only thing that holds those two together. Which means when the interface changes, as it just did, the test breaks — loudly, in a place someone is looking — instead of the promise quietly becoming false.