Twenty One Media
webAugust 25, 2026

Three Planning Rounds Were Not Enough. A Test Was.

When we repointed the site to new positioning last week, we had to pull retired copy out of roughly 30 files. Hero text, email templates, form labels, a live ticker component, city landing pages. The kind of change that feels manageable until you realize your mental model of the codebase is incomplete.

We planned it three times. Each time we scoped the sweep by hand, listing out the files we thought mattered. Each round missed something.

Round one: enumerated the homepage's constant arrays and caught most of the hero. Missed an entire guarantee section hardcoded directly in the component and a string sitting in HeroLiveCard.tsx.

Round two: enumerated pages. Caught most of them. Missed /book, /contact, and two email templates under src/lib/email/.

Round three: ran a real file walk, but rooted it at src/ without justifying why that boundary was correct. Missed 34 files under content/.

Each round felt more thorough than the last. Each round was still wrong.

The fix wasn't a fourth round. It was a test.

What the Test Does

We added tests/unit/retired-offer-language.test.ts. It walks every .ts and .tsx file under src/, checks each line against a list of banned terms, and fails if it finds one.

The banned terms include retired offer names, vendor citations we no longer make, and reach claims that measurement doesn't support. Each term in the list carries the reason it's banned: not just /n8n/ as a blocked string, but /n8n/ — we build everything custom; never cite an automation vendor. The test documents the decision, not just the rule.

The search root is explained in a comment, because an unjustified boundary is exactly what failed in round three. content/ is excluded explicitly, with a written reason: those blog posts document how the old funnel was built. They're accurate history, not marketing copy. If the blog ever comes back to the nav, the comment tells the next developer to add it to the scan.

The Allowlist Forces Honesty

The test supports an allowlist for files that aren't clean yet. Every entry requires a written reason. A file you're not ready to fix stays on the list. A file that becomes clean gets caught by a third test case that fails the moment the entry is stale.

The allowlist is currently empty. Every retired surface was deleted rather than excused. If something gets added later, it has to come with a written reason, and that list shrinking is the definition of done for any follow-up work.

That structure matters. If you can silence a test failure without writing down why, the list grows silently. If every entry needs a reason, the list stays honest.

Why This Type of Test Is Worth Writing

Most teams don't write tests against their own copy because it's not testing code behavior. It's testing content policy.

But content policy in a software product is enforced by the codebase. If it lives in a Notion doc or a planning comment, someone will miss it the next time a developer touches a file. If it lives in a test, CI catches the miss automatically.

We knew this codebase well. We planned the sweep three times with adversarial intent. We still missed 34 files because hand-enumeration cannot cover a codebase.

The test does what the planning rounds couldn't: it covers every file, every time, without relying on someone remembering the scope.

The migration is done. The test stays.