niblr
← Blog
Mayank

My tests passed. My code was still wrong.

Four bugs I shipped or nearly shipped in one week building Niblr — every one of them with a green test suite. What they had in common, and what I changed.

I spent a week rebuilding how Niblr stores and serves images. Four things went wrong. Every one of them had a passing test suite and satisfied its written acceptance criteria. Three I caught before they reached anyone; one took a live storefront's images down for a few minutes. Here is what happened, because the pattern is more useful than the fixes.

What actually went wrong?

A migration that would have deleted a file and reported success. I was moving generated documents into their own storage collection. The migration matched each document to its order by reading an identifier out of the filename. It worked in every test. Then I ran it against real data and found one record whose filename and whose stored identifier disagreed — I still do not know which edit separated them. The match returned nothing. The migration would have deleted the original, created an orphan, left the order with no document attached, and exited with a success code. No error, no warning, no test failure.

Image variants that came out in the wrong format. I added automatic processing so uploads get converted and resized. The acceptance criterion said the generated sizes must exist with URLs and widths. They did. They were also all JPEG, while the main file was WebP, because the format setting only applies to the original unless you repeat it per size. The test suite was happy. The whole point of the work — smaller files for shoppers — was quietly not happening.

A repair tool that would have re-broken what it repaired. Storage usage is tracked by a counter. I fixed the counter to include the newly generated files. What I nearly missed is that a script already existed to recompute that counter when it drifts — and it still used the old, wrong sum. Running the repair tool would have overwritten every correct value with an undercount. You reach for a repair tool precisely when you do not trust the data, which is the worst possible moment for it to be wrong.

And the one that got me. Reprocessing changed filenames. Cached pages kept pointing at the old ones. For a few minutes, a live storefront served broken images on every product. The script that caused it reported success, because from its point of view everything had worked. I found it because I opened the page. I would not have found it otherwise.

Why did the tests pass?

Because the tests asserted what I had written down, and what I had written down was incomplete in exactly the way my understanding was.

Every one of these bugs lived in the gap between a rule and the world. The migration's rule — match on the identifier — was correct. Reality had a record where the identifier had two different values depending on where you looked. The format rule was correct. It just had a scope I had not read carefully. The counter rule was correct in the new code and stale in the old script I had not thought to check.

You cannot test your way out of that, because the test encodes the same assumption as the code. A test written from a specification will pass for any implementation that satisfies the specification, including a wrong one.

What caught all four was the same thing: looking at the real output. Querying production data instead of fixtures. Loading the page instead of reading the exit code. Checking the format of the file that actually got written.

Does this mean the platform is fragile?

The opposite, I would argue, though you are entitled to your own read.

None of these are exotic. They are ordinary integration bugs of the kind every system has. The question is not whether they occur — it is whether anyone is looking hard enough to find them before you do, and whether they say so afterwards.

There is a related lesson from the same week. A private document was stored in the same collection as public assets, and inherited that collection's public read permission. Nothing about the code was wrong in isolation; the two decisions were made months apart and were individually reasonable. Sharing a container means sharing its defaults, including the ones nobody restated. I found it, closed it, and then rebuilt the boundary so the two things cannot share a default again.

That is the kind of thing you only find by going looking. I would rather tell you I went looking and found something than let you assume I never had to.

What I changed about how I work

Four habits came out of the week, and they are cheap:

  • Verify against production data, not fixtures. Two of the four bugs were invisible in any test database, because the test database did not contain the awkward record.
  • A script's exit code is not evidence. After anything that rewrites files or rows, load a real page and look at it. My outage lasted as long as it did because I read a success message instead.
  • Guard the destructive path, then set the guard off on purpose. Every script that deletes something now refuses to run when the database and the storage it is about to modify belong to different environments — and I test that refusal rather than assuming it.
  • Pin the coupling, not the intention. Where two files have to agree, a comment does not fail a build. A test does.

Why am I telling you this?

Because Niblr's pitch is that you own your store — your domain, your data, your payment keys, your code, self-hostable under an MIT licence. That claim is only worth anything if I am equally straight about the parts that go wrong.

A changelog that only lists wins is marketing. This is the other half of it. I will keep publishing both.

If you want to read how any of it actually works, it is all in the docs — or you can start a store and hold me to it.