Skip to content

Parity tests without a shared validator

Adityo Guni Waluyo

Two validation gates stay separate, but a parametrized test table binds both to the same accept/reject contract and the same error precedence.

TL;DR

Two standalone modules rejected bad configs differently, and sharing validation code at runtime wasn’t allowed due to isolation rules. Instead of merging code, a parametrized pytest table now feeds hostile inputs to both, ensuring they agree on accept versus reject. It also enforces resolve-first ordering where path traversal errors beat file shape errors, keeping docs updated and modules isolated.

The Moment at the Terminal

I was staring at a wall of red pytest output on my screen. Two modules, plane-init and plane-doc-sync, both reject broken configuration, but their error messages differ by exactly one space or a different word order. My first thought: this should just use one shared validator, so everything stays consistent. I nearly refactored the code to extract that validation logic into a single shared runtime module. It felt reasonable: if the code is one, the result must be the same.

The Wrong Guess and the Contract That Actually Matters

Turns out, forcing a shared runtime would have violated this project's architecture rules. Cross-skill imports at runtime are flat-out forbidden. These modules were deliberately designed to run standalone, without pulling dependencies from each other when they execute.

That's where it clicked for me: what matters is not how they reject, but what they reject. The contract is the accept/reject outcome, not string-equal error messages. Instead of merging the code, I'd rather merge the tests. I turned the plane-init pages gate into a parametrized table in pytest. With that, one identical set of adversarial configurations gets fed to both modules [1]. Pytest parametrize is really comfortable here because it supports multiple argument sets without writing a messy testing loop. If module A rejects and module B also rejects, parity is achieved. It doesn't matter that internally they build their error messages differently.

Error Order Matters

While writing this adversarial table, I ran into an interesting case. There was one scenario where the given path broke the traversal rules, say trying to escape the allowed directory, while at the same time the markdown file format didn't match the required shape.

The first module reported a file shape error. The second module reported a path traversal error. Parity failed.

I was briefly confused: which one should be checked first? After digging through it, path resolution has to finish before checking the file shape. That makes a lot of sense. If you check the contents or the extension before you're sure the path is safe, you hand the system a window to perform a read somewhere it shouldn't. pathlib helps a lot here because it provides path semantics that match the OS, so I never have to think about forward slashes versus backslashes manually [2]. So, once the path is resolved and proven safe, then we care about whether it's a valid file. This resolve-first pages parity principle became the standing rule in the test table: the traversal error always wins and shows up before any document shape validation error.

Docs and Isolation That Stay Intact

After the test table went all green, the next step was just updating the v3 docs scoping [3]. No runtime logic changed, no cross-import leaked. Module isolation stays whole, and now we have a test-level guarantee that both gates behave identically when facing hostile input.

This approach taught me one thing: consistency doesn't always have to be achieved by centralizing code. Sometimes consistency is proven just by a strict test contract. We let the modules stay autonomous, but we bind them to the same expectations at the tip of the testing spear.

Sources

Related articles