Verification
How this project checks itself, and the layer that was missing.
The count that prompted this
Over one working session, nine shipped things did not do what they said, across about twenty-five commits. Roughly one commit in three was fixing an earlier one. That is not bad luck; it is a property of how the checking was arranged.
They sort into three kinds:
Unreachable — the mechanism exists and no person can invoke it. panicWipe and burn
had no button for weeks. The position control was absent from sign-on while the setting was
still being written on every sign-on. Setup demanded a Watchtower, so an operator who knew
nobody could not get in at all.
Uncomposable — two pieces built to work together that do not. entriesAbout produced
exactly the shape verifyChain must reject. Peer presence read its relays from the
Watchtower config, so the feature built for people without a watch required one. The
escalation ladder stopped on an agent's acknowledgement.
Unmoored — a sentence with nothing behind it. The Status screen promised a cached directory, playbooks and a log; none existed. The status page's component list was wrong on five of six lines. The manifest said the app was installable with an empty icons array. The "load-bearing" daily rebuild built an artifact nothing consumed.
What the tests actually cover
| Layer | State |
|---|---|
| Pure logic | Thorough — 201 tests in core alone |
| Rendered HTML content | Good — assertions run against the built artifact, not the source |
| Anything a person does | Nothing |
There is no layer that opens the app, operates a control, and checks what happened. A control can be fully wired, fully typed, imported by the page, and simply not rendered — and every test passes.
The nine are not nine bugs. They are one missing layer, appearing nine times.
Move 1 — Browser-level tests — done
Playwright against the built static site, 28 tests in about fifteen seconds. This is the missing layer, and it is the only one of these that is architectural rather than clever.
It found a real bug on its first complete run. Area pages are cached on request rather than precached, and the mechanism relied on the document being fetched. SvelteKit navigates on the client, so tapping through to an area fetches its data and never its HTML — the document was never cached, and "opening an area is what saves it" was false for the only path anybody actually takes. Reloading by hand cached it; nobody reloads by hand. The page now asks the worker to save it, explicitly, once it has rendered.
It also found two failures in itself, which is worth recording because a harness that
quietly undoes the thing under test is worse than no harness. addInitScript runs on
every navigation, so seeding storage unconditionally erased a patrol between recording
it and navigating to look at it — and the failure looked exactly like the app not saving
patrols. It now seeds once and gets out of the way.
Shape
- Chromium only. WebKit would be closer to the iPhone truth and doubles CI time and install size. Revisit if a Safari-specific failure ever appears; do not pay for it up front
vite previewservingbuild/, so the tests exercise exactly what deploys — the same discipline the HTML assertions already follow- Tests live in
web/e2e/, away fromsrc/**/*.test.tsso the two runners do not collide - Device state is seeded with
addInitScriptwritinglocalStoragebefore load. There is no server-side state to set up, which makes this much cheaper here than in most apps
Four files, matching the three failure kinds
| File | Catches |
|---|---|
reachable.spec.ts |
Every control a capability claims is on its screen and operable |
flows.spec.ts |
Sign on → status shows on station → stand down → the patrol is recorded |
independence.spec.ts |
With empty storage and no Watchtower, the app works |
offline.spec.ts |
Service worker serves the shell and the directory with the network off |
The offline one needs care in sequencing: load the page, wait for the service worker to
reach activated, then go offline, then navigate. Going offline before activation tests
nothing and passes.
Where it runs
Inside npm run verify, not beside it. A check that only runs in CI is a check nobody
sees fail until after they pushed. If the browser is not installed the suite fails with the
one-line command that installs it, rather than skipping quietly — a skipped test is an
unmoored claim by another route.
CI adds npx playwright install --with-deps chromium, roughly two minutes.
What it also buys
It automates half of Milestone 0. "Airplane mode, cold start" and most of "carry it for a night" become machine-checkable. Two devices on one relay and the daemon-plus-executor run still need hardware — but they are two items, not four.
Move 2 — A capability manifest — done
One small data file. Each user-facing capability declares what it claims, which screen exposes it, and what it needs.
interface Capability {
name: string;
/** The route that exposes it. */
screen: string;
/** Sentences that must appear there — the claim and its limits, together. */
claims: string[];
/** A selector for the thing a person actually operates. */
control?: string;
/** What must exist for it to work. Empty means it works with nothing. */
requires: ('identity' | 'watch' | 'peers' | 'network')[];
}
Three checks derive from it, and none is written by hand per capability:
- The screen is in the build. Static, cheap, already half-implemented
- Every claim appears on that screen. Static, and this is what caught the cached directory that did not exist
requires: []is honoured — two ways. The module does not import the watch or config modules (import-graph, already done once for the offline directory page), and a browser test with empty storage operates the control successfully
The manifest is the data; Playwright is the engine. Item 3 is the one that catches peers-needing-a-watch, and it cannot be done statically.
Proved by breaking it. Renaming the position control's id made the suite fail with
#share is not on terminal/sign-on/ — the exact bug that shipped, caught by the exact
mechanism built for it. A guard nobody has watched fail is a guard nobody should trust.
One consequence worth naming, because it reads as a limitation and is the opposite: claims
are checked against prerendered HTML, so they cannot sit behind {#if}. Five times this
session an important sentence hid behind a conditional a fresh visitor never reaches. Putting
the claim where the test can see it puts it where the operator can.
Move 3 — A deploy stamp
The build writes version.json into its output: the commit it came from and when it was
built. A post-build script, taking the SHA from VERCEL_GIT_COMMIT_SHA where it exists and
git rev-parse otherwise.
Small, and it fixes two things.
"What is live" becomes one request with a definite answer. Right now there is no way to ask, so the answer was inferred from whether a particular string appeared on a page — which meant polling production repeatedly, which is what tripped Vercel's bot mitigation and made the site serve a challenge page. The absence of this mechanism caused that.
The daily rebuild becomes self-reporting. The status page shows how old the build is. A stamp three days stale says the scheduled rebuild is not running — which is exactly the gap found by reading the workflow, and it would have announced itself.
Move 4 — Fold the guards that Move 2 subsumes — done
Four hand-written tests are four versions of one idea:
- never claims a capability that is not built
- keeps the relay stack off the offline directory page
- every terminal screen is available offline
- the status page does not describe shipped things as not built
Each was written after the failure it now guards. Together they are the manifest, discovered one incident at a time. Folding them in is a deletion, which the rules here prefer to an addition.
Five folded. Two did not, and the second is the interesting one:
"No search box on the field terminal" is a prohibition, not a claim. There is no capability to attach it to
"Never claims a capability that is not built" points the other way. The manifest checks that a declared claim appears on its screen. That test checks that a claim made anywhere has a mechanism behind it — the direction that caught a Status screen promising a cached directory, playbooks and a log while none of the three existed.
Folding it in on the grounds that both are "about claims" would have quietly dropped the guard, which is the exact failure this whole page is about. It stays, with a comment saying why it looks redundant and is not.
A fifth thing, found by the tests themselves
Not planned. It surfaced while building Milestone 3, and it is the first failure this layer caught in itself rather than in the app.
Every browser test was racing hydration. Each terminal screen is prerendered, so its controls are on the glass — visible, enabled, and wired to nothing — for a moment after the HTML loads. Playwright fills and clicks far faster than a person does, and landed in that window. Under load it did so often enough that specs began failing at random: a peers test, then a patrols test, each looking like a bug in a different screen and each passing when run alone.
The fix is not a timeout. The terminal layout now sets data-hydrated when it mounts, and
the test helper waits for it — waiting for exactly the thing that must have happened, so it
cannot pass early on a fast machine or fail late on a slow one. The suite went from
intermittent to fifty consecutive green, and got roughly twice as fast, because tests were
previously sitting in retry loops against controls nothing was listening to.
Three things worth keeping from it:
- A flaky test in a suite with
retries: 0is a broken test, and re-running until green is how a project learns to disbelieve its own failures - One of the failures was real. Pairing was a
<form>submit, and a form tapped before hydration does a native GET: the page reloads and the typed code is gone. It is now a plain button, which does nothing until it works — inert beats destructive, and what a prerendered screen does before its JavaScript arrives is a design decision - The suite had been opening WebSockets to
relay.damus.ioon every run. It now stubsWebSocketdead. A test that can fail because a stranger rebooted a volunteer-run box is not a test, and the stubbed state — a pool whose socket never opens — is exactly the phone with no signal that most of these tests are about
A second list had also drifted: the offline spec named the terminal routes itself, under a
comment claiming it used TERMINAL_ROUTES. Two screens shipped without being added to it —
the precise failure routes.ts exists to prevent, defeated by copying it.
Order, and why
3 → 1 → 2 → 4.
The stamp is small, independent, and removes the reason production was being polled. The browser layer is the substance. The manifest needs the browser layer to check the half that matters. The folding needs the manifest to fold into.
What none of this covers
Said plainly, because a verification page that overstates itself would be the joke writing itself:
Two devices on one relay.
Peer presence has never crossed a real relay— partly closed.npm run test:relaystarts a NIP-01 relay on this machine and runs two browser contexts through it on real sockets: aREQthe client composed,EVENTframes it signed, and presence arriving at a subscription the second device opened before the first had anything to send. It also asserts that nothing readable about the operator is sitting in the relay's memory afterwards, which every other test could only assert against a stub that never had the chance to leak.What is still open is the half that needs a body: two phones, two networks, a relay somebody else runs. Public relays differ in filter handling, rate limits and retention. Build order
0.2standsThe daemon and the executor together. Both subscribe to
20911; that they do not confuse a client is reasoned, not observedCarrying it for a night. Nothing here finds text that is too long to read in the cold, a flow with a step too many, or a control in the wrong place
iPhone. Chromium is not WebKit, and the two differ most in exactly the places this app leans on — service workers, storage eviction, and
BarcodeDetector
Two found while building the cold start, 2026-08-23
Both are the same shape as the nine, and neither would have failed a test, because in both cases the code was correct and nobody could reach it.
An empty region had no page. entries() prerendered only regions that already held
records, and the directory index filtered the empty ones out — both correct decisions when
the only way to fill a region was a maintainer with a CSV, and both silently fatal the moment
an operator could add a place from the app. Thirty-five of sixty-eight regions were
unreachable, so the person with the local knowledge got a 404. A mechanism nobody can reach
is not built now has a fourth instance, and this one was reachable-in-principle by an
operator who typed the URL, which is what made it easy to miss.
The guard is in e2e/adding-a-place.spec.ts, and its first test is not about the form —
it is that Nashville answers at all.
This laptop cannot run the suite. Twenty-eight tests across twelve files fail here with
crypto.getRandomValues must be defined, and Playwright refuses to start; the working tree is
clean and CI pins Node 20 while this machine runs 18.16. So the failures are environmental and
long-standing, and the consequence is not:
Four EIN submissions said every claim rested on one laptop. It is worse than that — the laptop could not execute the suite either. "1,121 tests" was a true statement about a machine nobody had run them on since the billing lock landed on 2026-08-19.
That is the same defect class the EIN round catalogued in all five projects — a verification claim with no live counterparty behind it — sitting in this repository while its artifacts described everyone else's. Running under Node 22 gives 395 unit and 269 browser tests green.
Two things follow. Clearing the CI billing lock is not bookkeeping; it is the only environment
that can currently prove anything. And .well-known/navcom-health.json reports
"suites": {"ran": "unknown"} and "built_on": "local" rather than omitting the question,
because a receipt that cannot express "local, and stale" is worth nothing.
A third, from the CAR packer
Smaller than the other two and worth recording because of its direction. The directory packer
carries a comment explaining that walk() sorts because a UnixFS directory's identifier
depends on the order its entries were added — so an unsorted walk would produce a different
CID for identical bytes depending on what the filesystem returned.
A test written to prove that failed. ipfs-car sorts its own entries, so the identifier is
already stable across enumeration order, and the reason given in the comment was wrong.
The claim was wrong in prose before it was wrong in a test, which is the direction almost everything in this file has come from. The test was inverted rather than deleted — it now asserts the property that is actually true and will notice if a future encoder stops sorting — and the comment now says why the sort stays anyway: the file list published beside the identifier would otherwise reorder itself per machine.
Worth generalising: a comment explaining why something is load-bearing is a testable claim, and this project has now been wrong about one twice in a day.
The browser suite cannot tell a regression from a busy afternoon, 2026-08-25
Found while pushing four commits' worth of this session's work, and it earns its own entry because the evidence is unusually clean: the same suite, on two different trees, on the same machine, in the same hour.
verify failed with three browser-test timeouts. Every failure was a ~30-second wait, never a
wrong assertion, and a different test failed on each retry — story-new-phone and
story-second-holder, then standing twice, then standing once more at a different line. That
shape is not a regression; a regression fails the same thing every time. So the four commits were
checked out one at a time against a fresh packages/core build, and each built independently —
which ruled out a compile-time break but not a runtime one.
The conclusive test was to stash everything, check out 644604c — the commit this session
started from, nothing of this session's work applied — and run the full browser suite there:
| Failures | Passed | |
|---|---|---|
| HEAD, four new commits | 3 | 266 |
| 644604c, none of this session's work | 7 | 249 |
The clean tree failed more than the working one, on the same class of timeout, including
standing.spec.ts in both runs. That settles authorship — nothing in this session's work is
implicated — and it turns up the actual finding: this suite cannot currently distinguish a
regression from a loaded machine, and it has apparently been unable to for a while, since
644604c predates every commit this session made.
The consequence is sharper than it would have been a day ago. CI is declined (see 9.9) and
verify:deploy does not run the browser suite at all, so nothing exercises these 269 tests
between deploys except a person choosing to run them. The tests that flake are the ones
covering standing, credential handover and restoring a phone — not the least consequential
corner of the suite.
Not fixed here, because the fix is a real question rather than a quick patch: raise the
per-test timeout, reduce parallel workers, or accept that this machine cannot run the full suite
without contention and treat a red run as inconclusive until repeated. Recorded so the next
person who sees three red browser tests reaches for this entry before reaching for git bisect.
The part that is not architectural
Several of the nine came from editing files by string replacement against text written from memory. When the pattern does not match, nothing happens and nothing complains — that is how the position control was lost while the rest of the same change applied.
The fix is not in this repository. It is to use an edit mechanism that fails loudly on a non-match for anything load-bearing, and to reserve scripted replacement for genuinely mechanical bulk work.
Architecture can compensate — Move 1 would have caught it — but it should not have to.