Skip to content

Notes · 22 August 2026 · engineering

The suite is bigger than the site

There is more test than there is site. The application is about 136,000 lines; the tests are about 217,000. Roughly one and a half lines of test for every line of the thing being tested, across 1,131 files and 16,419 individual tests.

Four and a half months ago there were none. The first commit on this repository was 4 April. There are 3,172 commits since, and the test count went from zero in April to 480 files by the end of May, 1,003 by the end of July, and 1,131 now. Coverage sits at 94.7% of statements and 97.2% of lines, with floors in the config that fail the build if it drops.

I want to write about why that is, because the honest version is more useful than the number.

Commit heatmap for local-hotel.com, 4 April to 22 August 2026
Every commit to this repository since it started. 3,172 of them across 103 active days — which also means about five weeks with nothing at all.

The graph is worth reading properly rather than admiring. The dark block in May is the run-up to launch, and 19 May is the worst of it at 124 commits in a day, which is not a boast — a day like that is mostly small corrections and is a sign of not having thought something through beforehand. The trailing-off since is not slowing down; it is the shift from building the thing to running it, where a day's work is more likely to be one careful change than forty scrappy ones. There are also about five weeks of blank squares in there, which is what the rest of a life looks like.

What that bought, and what it did not

The growth is the story, and it is worth saying what it is for. Yesterday I ran a coverage exercise across the least-tested parts of the site: around 500 new tests, and a dozen files that had never been touched taken from nothing to 90-100%. Coverage went from 92.3% of statements to 94.7%, and from 94.9% of lines to 97.2%.

The interesting part is not the percentage. It is that the exercise turned up four real defects, none of which were the goal — a page with a syntax error that had survived every test because nothing imported it, a rate rule and a host dashboard that were both a day out for anyone west of Greenwich, and a calendar whose on-screen instruction told hosts to do the opposite of what they meant. They surfaced because writing a genuine test for a page means reading what that page actually does, closely, which is a thing nobody does often enough to a screen that already works.

A necessary caveat, because the rest of this argues for precision: nobody was harmed by any of it. The host side of the site has no hosts on it yet — no properties, no rate rules, no blocked dates — so the two that touch money and inventory were latent rather than live in the sense of anyone paying for them. I would rather say that plainly than let "four defects" imply casualties. Finding them before there is anyone to hurt is the entire point of looking.

And the coverage gate itself — the thing that fails the build when code arrives without tests — had never actually run. It sat in the config, correctly configured, evaluated by nothing, because the check before a push ran the tests without the coverage flag. Coverage had drifted below all four of its floors and nothing said a word.

What went wrong with the tests, specifically

None of this is an argument against testing. It is an argument against reading the number and stopping there. Four failure modes did the damage, and they are worth naming because each has a cheap fix.

Tests that cannot fail. I wrote one yesterday that "simulated" a visitor in another timezone by passing an option name I had invented. Intl ignores unknown options silently, so every assertion ran in my own timezone and passed — against the broken code. It looked like proof and was decoration. The habit that fixes this is small: before keeping a test, break the source on purpose and watch it go red. If it stays green, it is not testing what you think.

Mocks kinder than reality. A stub for the image component quietly dropped the `onError` handler. So the entire dead-image path — the code that reacts when a photo fails to load — could never run under test. Coverage reported it uncovered, which was accurate, but the reason wasn't a missing test. The test existed; the mock had deleted the behaviour it was meant to exercise. A mock that answers every question the same way lets a wrong question pass.

Zero per cent is a real signal; ninety-five is not a promise. The files that hurt yesterday were mostly at zero, and zero is easy to see. The trouble is that a high aggregate makes it comfortable not to look at the distribution underneath it.

A gate that does not run is decoration. This is the one I would put first if I were starting again. It doesn't matter how good a threshold is if nothing evaluates it at the moment code leaves the machine.

The thing that actually found them

Not looking harder. Changing where I stood.

Running the identical suite with the machine's timezone set to America/Los_Angeles turned green tests red in seconds, and each red one was a real bug. That check now runs on every push — the date-sensitive tests, in a non-UTC zone, in under two seconds. It cost almost nothing and it would have caught all four.

The X preview cards were similar. Every server-side check I could think of said they were fine: correct status codes, correct dimensions, correct headers, allowed by robots. They were fine. What was wrong only became visible when I looked at the actual timeline and noticed that one kind of post rendered an image and another did not. The difference was a question mark in a URL.

That is the pattern. Most of what a mature test suite misses is not hiding in the code. It is hiding in an assumption the tests share with the person who wrote them — same timezone, same browser, same happy path — and the way to find it is to change the environment and see what disagrees.

So why keep 217,000 lines of it

Because the alternative is worse, and because of what the suite makes possible rather than what it prevents.

I ran an exercise yesterday to raise coverage on the least-tested parts of the site. It added around 500 tests. It also turned up four genuine bugs — including the calendar one that would have quietly taken rooms off sale — and none of those were the goal. They surfaced because writing a real test for a page means reading what the page actually does, closely, which is a thing nobody does often enough to a screen that already works.

That is the return. Not that the tests catch everything, because they demonstrably do not. It is that they make change cheap enough to keep looking. Twenty-two changes went out yesterday, each one gated by lint, types, the full suite, coverage and the timezone check before it could reach the remote. The gate takes about three minutes. It is the reason a day of that pace is not reckless, and it is the part I would keep if I had to throw the rest away.

The suite is bigger than the site. It is still not big enough to think with instead of thinking. Both of those are true, and I would rather say so than quote the coverage figure and leave it there.