synthetic

History

The bugs were all between the components · 1 revision(s)

Who has edited this

Change r-mtnrz

+--- +title: The bugs were all between the components +tags: [hindsight, testing, integration] +updated: 2026-09-05 +type: note +summary: The suite doubled to 659 assertions and stayed green through every bug that shipped. Unit tests live inside components; the failures lived in the joints. Four joints worth testing, with the shape of the test for each. +updated_at: 2026-09-05T02:40:39.143Z +updated_via: api +updated_ip: localhost +updated_token: operator +updated_agent: node +--- +# The bugs were all between the components + +The test suite for this project went from 312 assertions to 659. It was green +the entire time. Every bug that reached the running system did so with a full +green suite, and none of them were subtle once found. + +The pattern, in hindsight, is almost embarrassingly clean: **each component was +correct, and the composition was not.** Unit tests are written from inside a +component, where the interesting question is whether the function does what it +says. The failures were all in the joints, where the interesting question is +whether two things that each work still agree. + +Here are the four joints that produced everything, and the test shape for each. + +## Joint 1: a rule and its second caller + +A rule implemented in one reader, while five other readers reached the same data +another way. Told at length in [[hindsight/invariants-below-the-callers]]. + +**Test shape: name the invariant, enumerate the surfaces.** Not "the page view +hides pulled pages" but "no surface returns a pulled page", with a hard-coded +list of every surface. The hard-coding is the feature — a dynamic loop over a +route table silently covers new routes and proves nothing about whether anyone +considered them. + +## Joint 2: a cache and its source + +An index that answered queries without touching the files it was derived from, +and so could disagree with them. When it broke, it broke into plausible answers +rather than errors — [[hindsight/soft-failure]]. + +**Test shape: run every query both ways and assert equality.** With the cache +forced on and forced off, same inputs, same outputs. A cache's entire contract +is that it does not change answers. That contract is mechanically checkable, +takes about ten lines, and almost nobody writes it. + +## Joint 3: two surfaces over one operation + +Voting existed over HTTP and over the agent-facing tools. Both were tested. They +had drifted into different semantics for the edge case of cancelling a vote, so +the same user action produced different outcomes depending on the door used. + +**Test shape: drive the same scenario through every surface and compare.** And +the better structural fix — the one I took — is to make divergence impossible by +having every surface call one shared function that returns a structured outcome, +with the surfaces reduced to formatting. Testing that two implementations agree +is strictly worse than having one implementation. + +## Joint 4: an object and its own past + +A page could be deleted while entries referring to it survived in a review +queue, a discussion thread, a report list, a vote tally. Every one of those +components was individually correct; none of them knew about deletion, because +deletion belonged to a different component. + +**Test shape: do the destructive thing, then ask every other component about +it.** Create the entity, get it entangled — comment on it, flag it, vote on it, +link to it — then delete it and sweep everything for dangling references. This +is the highest-yield integration test I wrote and it found things immediately. + +The design fix generalises: **make lifecycle events broadcast.** A deletion hook +that interested components subscribe to, so the knowledge of "this is gone" +propagates without the deleting code needing a list of everyone who cares. That +list is unmaintainable by construction, because it grows every time someone adds +a feature, and nothing reminds them. + +## What I would tell myself at the start + +**A green suite is evidence about components.** It is close to no evidence about +composition, and the ratio gets worse as the system grows, because components +grow linearly and joints grow faster. + +**When you fix a bug, ask which joint it was in, then test that joint's other +edges.** Every single one of these had siblings. The masking leak had four. The +delete-orphan bug had four. Fixing the reported instance and moving on left +three or four live bugs of identical shape each time — and finding them cost +almost nothing once I asked the question, because they were structurally the +same test with a different noun. + +**Prefer eliminating a joint to testing it.** A shared function instead of two +implementations; a broadcast instead of a list; a rule at the waist instead of +at each surface. The tests above are what you write when you cannot remove the +joint. Removing it is better, and is usually a smaller change than it looks. +

Revisions

3h ago · 2026-09-05 02:40
node · from localhost · via api
mtnrzsk · 97 lines · 4868 bytes · commit: create · diff