testing

One outside read, 70 files, one finding: the page inside two frames

Four local reviewers, 18 mutants, 67 live checks and 8,347 tests all passed a page that was drawing two navigation bars. None of them asked about frames.

A room with art on the wall
Photograph by Caleb Wikman on Unsplash

Everything I could think of ran before I asked anyone else to look. It still missed the thing a fresh reader saw in under eight minutes.

What I wanted to know

I wanted to know whether reviewing a branch thoroughly on my own laptop, before pushing it, would leave an outside reader with nothing to find. The method was four local reviewers, hand-applied mutants, live HTTP checks against a production build, two accessibility scans and a real browser, then one pull request instead of five. This post covers what that bought, the single thing it did not catch, and why none of it could have.

Hearso is a small real-time multiplayer trivia platform. The work on 2026-09-20 was a hostname split: marketing on www, the application on app, one deployment answering to both names, plus a new public status page at /health-status.

The rule for the night

Reviewer capacity was the scarce thing. One of the two automated reviewers we use draws on an allowance shared by the whole organisation (eight reads measured between 22:24Z and 02:27Z, about one every half hour) and after a 48-file read it refused every request for more than forty minutes. Eight pull requests wanted a turn.

So the rule was: review locally first, batch five branches into one pull request, and try to make the outside reader's first read find nothing.

What ran before I pushed anything

Four local reviewers read the unpushed branch, one brief each: correctness, security, test quality, repo conventions. Between them, 34 findings. Thirty-three are fixed on the branch; one is accepted with its reason written down, because it is about the cache headers of a 404 on a hosting platform and cannot be judged without deploying.

One more was mine, found while reading after them. A layout renders even when its page refuses, because the not-found boundary is drawn inside that segment's layouts, so a future layout under our operations area could paint data beside the 404 meant to hide it. I proved it with three planted files, then made the gate scan refuse that shape.

Then the mutants: break one thing in the code on purpose and check that a test notices. Eighteen applied by hand, eighteen killed. Each one counted only after asserting that the file's bytes had actually changed, because an unmutated file passing its tests reads exactly like a mutant that survived.

Then the live checks. One production build in split mode, a server on loopback, every request sent with the Host header a browser would send: 49 of 49 under the classic shell and 18 of 18 under the platform shell. In a real browser, the opt-in hostname run: 7 of 7. Both accessibility scans pass with the new page added to them.

The two expectations I got wrong

The first live run was 29 of 31, and both failures were mine, not the code's.

An unknown path asked of www is a 308 across to app by design: whatever is not on marketing's list belongs to the application, and the application answers the 404. My check had asked www for a missing page and wanted a 404. The fixed version asks both halves, and both are in the output.

The second is the one worth keeping. I checked that /changelog carried a footer link to the status page; it did not, so the check failed. Under the classic shell /changelog draws no footer at all, so "no link there" was true of the page and said nothing about the link. The check moved to /board, which does draw the classic footer, and passed.

Measuring instead of arguing about it

One reviewer raised GET //library in split mode: the helper that builds a cross-host link throws on a path whose second character is a slash, and nothing caught it, so our interceptor would answer 500. Fixing that raised a second question. A path like //evil.example/x names a different authority to a browser, so where would a redirect actually send someone? Rather than reason about it, I asked the running server: Next.js's own router normalises that path to Location: /evil.example/x (one slash, our own host) before our code runs at all.

The point is how it was judged. The script prints the Location and passes it to a small function that decides whether a browser following it stays on our two hostnames, and that function has ten control cases of its own. A status code alone would have said nothing: a 308 to somewhere else is still a 308.

I got the control habit wrong elsewhere the same night. I wrote that mail to one of our support addresses "bounced", when I had measured missing DNS records and nothing else, and my port-25 probe was worthless because its control (a real mail exchanger) read closed too.

70 files, one finding

The pull request opened at 06:37:19Z: 70 files, five branches of work, with a read requested the same second.

At 06:45:09Z, seven minutes and fifty seconds later, the first automated reviewer left exactly one comment, on line 68 of the status page:

app/layout.tsx always mounts SiteShell, but /health-status is in neither PLATFORM_ONLY_PREFIXES nor DUAL_SHELL_PATHS, so the root shell does not step aside here. This Viewer therefore renders inside the classic shell: platform mode gets duplicate nav/footer, while classic mode gets the classic nav/footer around the page's bare wordmark.

It was right.

Two frames

Think of a framed print. Our app hangs every page in the same frame: a navigation bar above, a footer below. A few pages arrive already framed, and hanging one of those inside the house frame gives you two.

Precisely: the root layout in Next.js wraps every route in the app, and ours mounts the classic site shell. A short list of paths tells that shell to stand down, for the handful of pages that mount a frame themselves.

/roadmap is on that list and draws its own bare wordmark. /health-status was modelled on /roadmap, mounts the same component, and was not on the list. So under the platform shell it came back with two navigations and two footers, and under the classic shell with the classic bar and footer wrapped around a bare wordmark.

Diagram: an unregistered self-framing page gets the root layout's frame and its own, two frames; registered, it gets one.

Figure 1. The same route twice, before and after commit 703aee91 (2026-09-20). The list controls exactly one thing at runtime: whether the root layout steps aside.

Why nothing I had written could find it

Every check I wrote asked that page two kinds of question: what status code does it return, and what words are in it. The accessibility scans add a third kind, and a duplicate landmark is not a blocking finding for them, so they stayed green as well.

None of them asked how many frames it had. Not the 67 live checks, not the four reviewers, not the 8,347 unit tests on that head.

Horizontal bars: six layers of local verification raised 0 findings about the page's frame; one outside read of 70 files raised 1.

Figure 2. Findings about the page's frame, by who was looking, on 2026-09-20 (UTC). The local layers raised 35 findings between them; none was this one.

Two days earlier I wrote up eleven checks whose "nothing happened" case read as success. This is not that shape. Every check here ran, reached a real running page and reported truthfully. They examined a different dimension from the one that was broken.

Same sentence, different edge: a check says what it examined. Mine examined the content and not the frame.

The fix, and the class behind it

Registering the route is the small half. It joins the list, the footer guard's fixture joins with it, and because that list controls one thing at runtime, nothing else about the page changes.

The class is the harder half. There was already a guard here: a hand-written map of path to page, compared against the list in both directions. It notices a path added to one and not the other. It can never notice a page added to neither, which is exactly what a new page is.

So the new guard reads the source tree instead. Every page.tsx outside the platform route group that mounts the frame component must be a path the root shell steps aside for, and the reverse. Eleven paths today.

expect({
  pagesExamined: pages.length > 10,
  selfFramed,
}).toEqual({
  pagesExamined: true,
  selfFramed: [...DUAL_SHELL_PATHS].sort(),
});

pagesExamined is in that assertion because a scan that finds no pages passes every claim it makes about them. Comments are stripped before the scan, so a sentence mentioning the component is not counted as mounting it. And the guard was checked the way any test should be: with the route taken back out of the list, it fails and names it.

The numbers after

The live scripts count frames now, and each new block carries a control. /board is a classic page and must be inside the classic shell; /roadmap, the page /health-status was copied from, must not be, and must have exactly one footer.

RunBuildChecksOf which count frames
classic shell, before76a2f0ae49 of 490
classic shell, after703aee9154 of 545
platform shell, before76a2f0ae18 of 180
platform shell, after703aee9122 of 224

Stacked bars: 49 and 18 checks passed before, none of them counting frames; after, 54 and 22, of which 5 and 4 count frames.

Figure 3. The same live scripts before and after, on 2026-09-20 (UTC). Every check passed in all four runs; the bars that changed are the ones that ask a question nobody had asked.

On the fixed head the whole suite is 447 files and 8,354 tests, with the type checker and linter clean.

Both halves are true

Local review first worked. Seventy files came back with one finding, where the two pull requests before it each needed a second read: one brought back a real bug that would have been an endless redirect, the other two smaller ones.

Local review first also missed something a fresh reader saw in under eight minutes. Both of those sentences are true, and I would rather write them down together than pick the flattering one.

The lesson I take is not "review harder". It is that a verification examines a dimension, and the honest artifact is the list of dimensions you did not examine. For a page, ours now reads: status code, headers, cookies, words, landmark count, and frames.

Key takeaways

  • A check examines a dimension, not a page. Write down which dimensions you did not examine, next to the ones you did.
  • Local review before pushing is worth it and is not a substitute for a fresh reader. Ours cost one finding on 70 files instead of a round trip per branch.
  • A guard built from a hand-written list can only notice things somebody already wrote down. To catch the thing nobody wrote down, read the tree.
  • Make a check state how much it examined. A scan that finds nothing passes every assertion it makes.
  • A negative result needs a positive control in the same command: a page that must be inside the shell, a mail exchanger that must accept mail.
  • Judge the thing you care about, not a proxy for it. A redirect's status code is not its destination.

What we would do next

Two habits, both cheap. A new public route has at least three registrations to answer for: the shell list, the sitemap and the accessibility scans. Only the first is now guarded by something that reads the tree. The status page was missing from both accessibility scans too, and I only caught that by walking the repository's own checklist, which is a habit rather than a check.

Then the reviewing habit. Before asking anyone for a read, write the two or three questions that nothing in the diff's own checks would answer. On this branch that list would have started with "what does this page look like", which is the one question 67 HTTP checks cannot ask.

One thing is honestly unfinished: /health-status has never rendered a real payload. It is verified against a stand-in, because the monitoring status page it reads has not been published yet, and the split-mode runs in this post only ever saw its unconfigured "not available yet" state.

Evidence

All times UTC, all 2026-09-20. Paths are relative to the web repository unless stated.

  • The review allowance: eight reads at 22:24, 22:55, 23:26, 23:59, 00:27, 00:58, 01:58 and 02:27, one refusal streak of over forty minutes after a 48-file read, and eight pull requests waiting: artifacts/plan/2026-09-20-overnight-handoff.md, section 0 and section 1 item 2.
  • 34 findings from four local reviewers, 33 fixed and 1 accepted: artifacts/plan/2026-09-20-local-review-ledger.md, section A. Correctness C1-C6, security S1-S10, test quality T1-T12, conventions V1-V6; the accepted one is S7, the cache headers of the operations area's 404.
  • The finding I raised myself (a layout renders when its page refuses): the same ledger, entry T13, closed in ecc69059, proven with three planted files.
  • 18 mutants applied, 18 killed, each after asserting the file's bytes changed: artifacts/plan/2026-09-20-combined-domain-pr-body.md, verification line.
  • 49 of 49 and 18 of 18 before the finding (build 76a2f0ae): live-run-2-classic-49-of-49-before-the-finding.out, live-run-platform-1-18-of-18-before-the-finding.out.
  • 29 of 31 on the first attempt, and the two wrong expectations: live-run-1-first-attempt-two-wrong-expectations.out; the corrected checks are in live-check-classic.sh, with the reason in a comment above each.
  • The //library finding (the link helper throws, so the interceptor answers 500): the ledger, security finding S2, fixed in 67db6ac5.
  • No open redirect, measured, and the ten-control safe_location function that judged each Location: live-check-classic.sh; the ledger's end-to-end evidence section; the eight redirect rows in live-run-2-...out and live-run-3-...out.
  • The port-25 probe whose control also read closed: artifacts/plan/2026-09-20-overnight-handoff.md, section 5.
  • 7 of 7 in a real browser, and both accessibility scans passing with the new page: the ledger's end-to-end evidence section (opt-in hostname run, split build, platform shell).
  • 8,347 tests across 446 files at the head the pull request opened on: the same section. On the fixed head 703aee91: 447 files, 8,354 tests, tsc 0, eslint 0, recorded in the commit message.
  • The pull request: 70 files, opened with a read requested at 06:37:19Z, one comment at 06:45:09Z, 7 min 50 s later: the hosting service's own createdAt for the pull request and its timeline's review_requested and reviewed events, read on 2026-09-20; the comment itself is quoted from app/health-status/page.tsx:68. (The ledger first said 06:36Z, which was written from memory and was wrong; it is corrected there.)
  • The fix: commit 703aee91, 2026-09-20T06:57:12Z. lib/platform/routes.ts, app/site-shell.test.tsx, lib/platform/dual-shell-footer.test.ts and the new lib/platform/self-framed-pages-are-registered.test.ts (115 lines added across four files).
  • The guard, its eleven paths and its pagesExamined assertion: lib/platform/self-framed-pages-are-registered.test.ts; with the route removed from the list it fails and names it.
  • 54 of 54 and 22 of 22 after (build 703aee91), with /board and /roadmap as controls: live-run-3-classic-54-of-54-counting-frames.out, live-run-platform-2-22-of-22-counting-frames.out.
  • The two pull requests before it: artifacts/plan/2026-09-20-overnight-handoff.md, section 3. #445 (One major finding, two origins accepted on one hostname, which would have been an endless redirect) and #446 (two findings, both fixed).
  • The status page has never seen a real payload: the same handoff, section 1, item 4.

Nothing above contains a credential or a real hostname; the live runs use www.localhost and app.localhost, and evil.example is a reserved example name.

Get the next one

We write these up when something is worth writing up: roughly once a month, never on a schedule. Every number in them comes from a run we can point at.

One email when there is something to read. Unsubscribe in a click.

HearsoHEARSO · LOADING

Loading, 0%

Never goes backwards. Never lies about being done. Under a second on a good day.

ASSETS · STATE · HANDSHAKE
Help improve Hearso

With your permission, we measure basic game usage, safe button/link interactions, and IP-based traffic data. We do not send your email, name, country, answers, or sign-in tokens to analytics. You can change this in Settings after signing in. Hearso also keeps anonymous totals of rounds, players, and live rooms without this permission; see the player guide.