Skip to content
Development

Testing the part of the codebase that documents its own footgun

By Victor Da Luz
astrotestingdev-logastro-tools

My affiliate-link package has a remark plugin that rewrites [text](affiliate:key) links to their real, tagged URLs at build time. Its README has a warning in bold: use the [plugin, options] tuple, not the pre-invoked form, because Astro and unified call the plugin function themselves with the options. Passing an already-invoked transformer means unified calls that with no arguments as if it were the attacher, which silently no-ops instead of rewriting anything. The build stays green with affiliate:key links left untouched in the output.

I wrote that warning myself, from experience. And until this week, the plugin the warning is about had zero test coverage. Two other modules in the same package had solid tests. The remark plugin, the one piece I’d already documented as having a silent-failure mode, had none.

Noticing the gap

It wasn’t a mystery bug this time, just an honest backlog scan: what in this codebase is undertested relative to how badly it can fail. The remark plugin stood out for a simple reason. I’d already written down what its worst failure mode looks like, in prose, in the README, months ago. A build that stays green while quietly shipping unrewritten affiliate:atomicHabits text straight into the rendered page. Not a crash, a silent no-op, the kind of bug a reader notices before I do because nothing in CI complains.

Documenting a failure mode and then never writing a test for it is a strange half-measure. The warning protects against someone doing the wrong thing by hand. It does nothing to catch a future refactor that breaks the same code in a related way.

What was actually easy to test

The plugin turned out to be a plain function once I looked past the “remark plugin” framing: it takes a small tree-shaped object and a file-like object with frontmatter.affiliates, walks the tree looking for links, rewrites their URLs, and throws if anything’s inconsistent. None of that needs a real Markdown parser or the unified pipeline running. A bare { type: 'link', url: 'affiliate:atomicHabits', children: [] } object exercises the same code path.

const link = { type: 'link', url: 'affiliate:atomicHabits', children: [] };
const tree = { type: 'root', children: [{ type: 'paragraph', children: [link] }] };

remarkAffiliate(config)(tree, fileWithAffiliates(['amazon']));

assert.equal(link.url, 'https://www.amazon.com/dp/B07RFSSYBH/ref=nosim?tag=example-20');

I nested the link inside a paragraph deliberately, not at the tree’s top level. The plugin recurses through node.children to find affiliate links wherever they are, and a flat top-level fixture would never exercise that recursion. It would have passed even if the recursive case were broken - the same trap as a test that proves nothing.

The four cases that mattered

The rewrite itself on a nested link. An unknown catalog key throwing, so a typo’d affiliate:atomicHabbits fails the build instead of shipping a broken link. A used program not declared in the post’s affiliates: frontmatter throwing, which is the actual compliance mechanism that makes an FTC disclosure impossible to forget. And a tree with no affiliate links at all doing nothing, so the plugin doesn’t accidentally require frontmatter on every post in a site.

Four short tests, no mocks, no fixture files, no unified() pipeline setup. Fifteen minutes of work for a piece of code I’d already flagged, in writing, as risky.

What I’d do differently

When I write a README warning about a specific failure mode, that’s the signal to write the test for it right then, not add a backlog item to revisit later. The warning is proof I already understand exactly what could go wrong, which means I already know exactly what the test should assert. Letting time pass between “I know this can fail this way” and “I wrote the test for that” just lets the knowledge decay before it gets used.

Related reading