Five ways to verify a fix without testing it
A passing check, a clean query, a plausible error, a success message. Each of these looked like evidence, and each was measuring something other than the change I thought I was testing. The habits I use now so that "verified" means something.
Most of the posts on this site are about a control that looked healthy and did nothing: a backup check that checked nothing, a firewall service that was enabled and never ran, a DNS server that was up and served no zones.
This one is about the same failure in my own process. Every item below is a way a check looked like proof that a change worked, when it was not — and the ones that happened cost real time. Each was real evidence, of something else.
1. I edited a file that nothing runs
There were two similarly named copies of the proxy checker’s orchestrator, in two directories. I worked in one of them for several sessions — changes made, results examined — before a grep of /etc/cron.d showed that the job actually running was pointed at the other.
Every conclusion from those sessions was about code that was not executing. Nothing errored, because the file I was editing was perfectly valid. It just was not the one anybody ran.
The habit now: on an unfamiliar system, before opening an editor, ask the system what it is actually running.
systemctl cat <unit> # the real ExecStart, with drop-ins
grep -rn '<script-name>' /etc/cron.d /etc/crontab /var/spool/cron 2>/dev/null
ps -eo pid,args | grep '[o]rchestrator' # what is running right now
2. The results can be older than the fix
Deploy a change, restart the service, query the results table — and the rows look right. The trap is that nothing in a result set says which code produced it. Rows that finished before the restart came from the old code, and they look exactly like rows from the new one.
A query returns what is in the table, not what was produced since you last touched something. Recent-looking rows are not necessarily rows from this deployment.
The habit now: select the timestamp, and compare it with the moment the new code started.
systemctl show -p ActiveEnterTimestamp <unit>
SELECT id, status, finished_at FROM queue
WHERE finished_at > '<ActiveEnterTimestamp>' ORDER BY finished_at DESC;
If no row is newer than the restart, there is no evidence yet — only a table.
3. rowcount said nothing happened
It is tempting to use an UPDATE as a probe: set a column to the value it should already have, and read rowcount to see whether the row is there. When rowcount comes back 0, it looks like the row is missing.
It does not mean that. MySQL’s affected-rows count for an UPDATE is, by default, the number of rows actually changed — not the number the WHERE clause matched. A row that already holds the value is matched and not changed. SET x = x reports 0 for every row it touches, and so proves nothing about any of them.
The habit now: to know whether a row exists, SELECT it. If you genuinely need matched rows from an UPDATE, ask for them — the CLIENT_FOUND_ROWS connection flag (client_flag=CLIENT.FOUND_ROWS in PyMySQL) changes the count to rows found. Know which one your driver is giving you before you read anything into it.
4. The vendor rejected a placeholder
A proxy check failed with 407 Proxy Authentication Required. That is exactly what a vendor sends when it rejects your credentials, so that is what it looked like — and it cost two debugging sessions to establish that it was not.
The test payload had been written with placeholders for the credentials, meaning to fill them in:
{ "proxy_username": "<USERNAME>", "proxy_password": "<PASSWORD>" }
They were never filled in. The checker built a proxy URL from them — URL-encoded, < and > become %3C and %3E — and sent it. The vendor correctly rejected a user called <USERNAME>. A perfectly plausible error, for a completely different reason.
The same thing happens with commands. <IP> written into a command for someone else to run does not fail as “you forgot to substitute this”; it fails as Expecting value: line 1 column 57, which sends everyone looking at JSON.
The habit now: never put a placeholder inside something executable. Test credentials are read from the database by the test itself, so nothing is typed — which also keeps real secrets out of shell history, chat and documents. And commands written for others are correct exactly as written, because the person running them will, correctly, paste them literally.
5. The API said it worked
The last one was not my code, but it fooled everyone who tested against it, and it is the most instructive. On the certificate platform, the endpoint on each server responsible for installing a transferred certificate returned Certificate synced. — and did nothing else. The real implementation existed in the deployment template; that server had simply never been redeployed with it.
So every earlier “end-to-end test passed” had been a conversation with a stub. The control plane recorded success. No certificate was installed.
It was found by testing the chain on the server itself, rather than trusting what the control plane reported.
The habit now: assert on the effect, never on the response. After an install, the certificate’s live/ directory must contain symlinks, and certbot certificates must list the lineage. A success message is a claim, not evidence.
The pattern
| What was checked | What it proved | What would have proved the claim |
|---|---|---|
| the file I edited | that file changed | the file cron or systemd executes |
| rows in the table | rows exist | rows finished after the restart |
rowcount | how many rows changed | a SELECT of the row |
| the error message | the vendor rejected something | the payload actually sent |
| the API response | the endpoint returned a string | the certificate on disk, and certbot’s view of it |
In every case the check was one step removed from the thing being claimed: a copy instead of the running file, a table instead of this run’s output, a count of changes instead of a count of matches, an error instead of the request, a response instead of the state.
The fix is the same each time. Before calling something verified, ask: what exactly would this check look like if my change had done nothing? If the answer is “the same”, it is not a verification.
Items 1–4 came out of the distributed proxy checker; item 5 out of the SSL certificate automation.
References
- MySQL C API:
mysql_affected_rows()— forUPDATE, rows actually changed by default; rows matched withCLIENT_FOUND_ROWS. - PyMySQL connection options —
client_flag. - systemctl(1) —
catandshow. - RFC 3986 §2.1: percent-encoding
- RFC 9110 §15.5.8: 407 Proxy Authentication Required