DEV Community

Cover image for Four Ways a Test Can Lie to You
Ken W Alger
Ken W Alger Subscriber

Posted on Originally published at kenwalger.com

Four Ways a Test Can Lie to You

AI coding reveals why green tests lie

Green and worthless: what verifying one small application taught me, when an AI agent wrote most of the code.

The Sanity DEV Challenge wrapped up last week, and The Cellar won Path Two. The judges called the build writeup “a study in making verification hard to fake,” which is a generous reading of what was mostly me discovering that my verification was broken.

My submission covered what I built. This is about the part of the build I keep thinking about: how often my checks were wrong.

I spent two weeks building a wine cellar application that can be read at any date in its history. One developer, one agent, about 530 tests by the end. Three of the four real bugs in the project were invisible to every check designed for the feature they lived in.

That was uncomfortable, so I went looking. What I found was worse than missing tests: checks that were green, or red, and meant nothing either way. There turned out to be four distinct ways that happens, and I hit all four in a project where I was being unusually careful.

Lab scientists have had vocabulary for this for a long time. An experiment needs a positive control, proof that your method can detect the effect when it's present, and a negative control, proof that it stays quiet when nothing is there. Each of the four lies below is a missing control, or a control pointed at the wrong thing.

First: the test that cannot fail

No positive control.

The application resolves a wine's drinking window from dated claims. Claims can be proposed by an agent and must be accepted by a person, and a proposed claim is supposed to change nothing until someone accepts it.

That is the load-bearing rule of the whole review workflow, so it got a test. Build the cellar, inject a proposed claim, assert that all 542 bottle states are byte-identical. Green.

It was measuring the fixture, not the rule. Resolution picks the highest authority tier first, then the most recent claim within that tier:

Claim Tier Dated Wins?
The incumbent personal 2024 yes
The one under test personal 2019 no, loses on recency

Both claims are the same tier, so the one under test loses on recency, and it loses whether it is proposed or accepted. Delete the entire review-state filter from the source and the test still passes.

What caught it was a companion assertion written for a different reason: accepting must change at least one state. That one failed, which is how I learned the first one was vacuous.

The rule falls straight out of it. Every test asserting that X has no effect needs a paired assertion showing the effect exists when X is enabled. Without the pair, you haven't tested that X is inert. You've tested that your fixture was inert.

An audit of the rest of the suite found two more of the same shape. One claimed to check that a bottle opened during a period doesn't count as a missed opportunity, but two separate mechanisms excluded that bottle, so the clause under test could be deleted without anything turning red.

I assumed that clause couldn't be isolated and told the agent to fall back to a weaker assertion. It ran the experiment instead, and found a fixture by realizing that "now" in this system is a parameter rather than the clock. As of June 2024, what had I missed that year? is an ordinary question to ask a time machine, and it made the case constructible. The fix was better than the instruction.

All three fixes were verified by mutation: delete the clause the test claims to cover, and watch it finally fail. The original fixture stayed in the suite, renamed to say what it actually tests, because a record of the mistake is more useful than a quiet deletion.

Second: the check that always fails

No negative control.

A week later, building a public static bundle, I added a grep of the built output for leaked API credentials. Five seconds to run, and the failure it catches is unrecoverable, so it seemed like an obvious win.

It fired on every clean build. The pattern was sk[A-Za-z0-9]{20,}, and it matched skipCrossDatasetReferenceValidation, a legitimate export name inside @sanity/client.

This is the mirror image of the first lie, and in practice it's worse. A test that never fails is invisible: nobody notices it isn't doing anything. A check that always fails is noticed immediately, and then it is dismissed, every time, by the person who knows it's a false positive. After a week, the real failure it exists to catch would scroll past with the rest.

A check that always fails certifies exactly as little as one that never can. It just looks more diligent while doing it.

The fix was small. The bound went from twenty characters to forty, which real credentials clear easily and that export name doesn't. Then the scan moved into CI, where it runs on every push instead of whenever someone remembers. A check that always fails is worthless, and a check nobody runs is worth the same.

In control terms, the original grep had no negative control: nothing showing it stays quiet on a build known to be clean. One run against a clean build was all it took to expose it, which is how it was found, by running it rather than shipping it. The other half, proving the tightened pattern still fires on a planted fake credential, is the test I'd add next.

Third: the correct test of a wrong contract

Both controls present, measuring the wrong thing.

This one is the hardest to spot, because there is nothing structurally wrong with the test at all.

The application has a date control spanning 1996 to 2042. Typing a date into it worked for month and day, and the moment you started typing the year, the field snapped back to 1 January 1996. You had to finish with the picker.

The cause is a genuinely surprising piece of browser behavior. A native date input never reports partial text. It reports a complete, well-formed date with the year zero-padded to four digits. Typing 2023 into a field already holding a month and day emits this:

0002-12-31   ->  clamped to the 1996 floor
0020-12-31   ->  clamped to the 1996 floor
0202-12-31   ->  clamped to the 1996 floor
2023-12-31   ->  accepted, but the field was overwritten three keystrokes ago
Enter fullscreen mode Exit fullscreen mode

Every intermediate value is a real, valid date. Every one is out of range. Every one got clamped, and the controlled input wrote the floor back over the edit.

There was a test covering this. It asserted that parseDateInput('1901-01-01') returns the 1996 floor. It passed, and it would have failed if clamping broke. Positive control, negative control, meaningful assertion, all present.

The contract it encoded was wrong. Clamping an out-of-range date is correct when the user has finished entering it and wrong while they're still typing, and the test had no way to know the difference because the specification didn't either.

That's the dangerous category, because the first two look like mistakes once you see them and this one doesn't. An assertion about clamping out-of-range input reads as diligence. It would survive code review. It survived mine. I covered this failure mode in more depth in The Contract Discovery Bottleneck.

Fourth: the right test against the wrong system

Controls present, pointed at something else.

To test the review workflow safely, I set up a staging copy of the dataset. The plan: point the authoring tool and the application at staging, propose a claim, accept it, and watch the right bottles change state, all without touching production.

The authoring tool honored the environment variable that selects a dataset. The application, built by the same command with the same variable set, silently ignored it. Its built bundle was byte-identical to a build with the variable unset, falling back to production. No warning, no error. It built and ran perfectly.

So the authoring tool would have written a proposal into staging while the application reported production. Every number on the screen would have been internally consistent, and about a different cellar.

No amount of rigor in the test helps here. The assertions can be perfect and still certify the wrong thing, because the question that came first, which system am I actually looking at?, was never asked.

The fix had two parts. The application's dataset became a hand-edited literal, so it can't be silently wrong: any change to it shows up in the diff. And the application now displays the dataset name in its header whenever it isn't production. The reasoning matters more than the fix. The question was never how to point it at staging. It was whether I was sure which dataset I had just written to.

The examples are the paths you already walk

Separate from all four, and the one I think about most.

The same date control had an arithmetic bug: the span between 31 January and 1 March computed wrong, because the implementation borrowed days from February and silently dropped the remainder.

I had five verification dates for that feature. They were chosen carefully, from real data, before the code existed. They're the dates in the demo video and the screenshots.

All five passed under the buggy version. The application worked. The screenshots were right. Every date I planned to show anyone was fine.

What found it was a property test across all 17,167 positions the slider can occupy, asserting an identity that has to hold for any pair of dates. The bug lived in a region nobody would drive to on purpose.

Demo-driven verification proves the paths you already walk. That's not nothing, and it's not what you think you're getting.

What actually worked

None of it was "write more tests."

Oracles generated before the implementation, with an explicit instruction not to repair them. Expected outputs computed independently and committed before the code existed. The prompt said to treat them as an oracle rather than as output to be fixed, and to stop and report on any disagreement. When a test fails, the cheapest available move is adjusting the fixture, and that instinct needs to be foreclosed in advance rather than resisted in the moment.

Mutation, but only on the tests that matter. Delete the clause the test claims to cover. If it stays green, the test is decorative. This found two of the three vacuous tests, and the agent started doing it unprompted after the first.

Measurement instead of reasoning, on anything with a number in it. A performance bug hid in four lines of correct-looking memoization that had already passed review. Rebuilding an index cost 6 ms; recomputing the thing the index served cost 0.07 ms. The code memoized the wrong one. Nothing about reading it revealed that. Benchmarking both took ten minutes.

A gate that named what would not count as proof. For checks that needed eyes on a rendered screen, I wrote out the full path being tested and then listed the ways it could be faked: don't satisfy this by calling the module from a script, querying the data another way, or pointing at the test suite. Then: you cannot see a browser, so do not report this as passed. Give me the command, the URL, and the expected numbers, and I'll confirm.

That last one matters more in an AI-assisted build than it used to. A model asked to confirm something it cannot observe is under real pressure to confirm it anyway. The fix isn't trusting it less. It's giving it an honest third answer between passed and failed: not verified, here's how you check. A result nobody could observe isn't a pass, and a harness that can't say so will round it up to one.

Agents are good. They aren't exempt.

The agent wrote nearly all of the code in this project, tests included. So it's worth being plain about who wrote most of the lies above: the same tool that wrote the code they were checking.

That isn't an indictment. Over two weeks the agent was better than I am at a lot of this. Before every stage, it read my specification against the platform and found errors in it; by the eighth session, that pass was turning up ten conflicts at a time, two of them outright mistakes in my spec. When I proposed testing a Function against production with a trigger scoped to a document type that didn't exist, it declined, correctly: scoping a trigger is safe, but the only way to invoke one is to write a matching document, and my plan would have left two permanent mutations in production's history to avoid creating a scratch dataset. It found the fixture I told it was impossible. It measured the cost of a feature I hadn't asked it to measure.

It was also wrong in ways a careful human would recognize. It confidently reported that an API couldn't do something the documentation demonstrated, then fixed it in one line once asked to check rather than assert. It reached the right conclusion about date comparison for the wrong reason, which is right only until the case changes. Asked for a list of the polish work that remained, it drafted the list from the build plan instead of the code, and three of the items were already done. A plan describes work that was scheduled, not work that is left.

And it broke rules. Twice, after being told not to run git commands that change the repository, it ran git checkout to undo its own uncommitted edits. Earlier, it had modified an oracle file it was explicitly told not to touch, then restored it. Every time, it reported the violation unprompted, before I found it. "The agent broke a rule and told me" is more useful data than either cleaner story, "it never broke a rule" or "it broke a rule and I caught it." The rule now says the file tools are the only way to revert anything, including its own edits.

The pattern underneath is the part worth keeping. An agent writes tests the way it writes code: quickly, fluently, plausibly, and from the same understanding of the problem. When one tool writes an implementation and its tests from the same prompt, they share one set of assumptions, and a wrong assumption gets encoded twice and then agrees with itself. That's the third lie, and agents make it cheaper to commit at scale. Speed doesn't remove the verification problem. It moves the bottleneck there, which I've written about before.

What changed the outcome wasn't trusting the agent less. It was making verification structurally hard to fake: oracles committed before the code, controls on every check, gates that named what wouldn't count, and final confirmation in the hands of the one participant with eyes. Under those conditions, the agent didn't just pass the checks. It started improving them.

It happened again

A month later I built a small experiment harness for something unrelated, comparing small language models against plain code on a Raspberry Pi. I knew this taxonomy by then. I found the same lies anyway.

A fixture that couldn't distinguish the behaviors it was meant to compare. A metric that rewarded a model for always giving the safe answer, which is a check that always passes wearing a lab coat. And a missing directory that ran an entire round of questions against an empty reference corpus, producing a detailed, confident, wrong write-up: the right test against the wrong system, again.

Knowing the categories didn't prevent them. It made them faster to recognize, which is the most I'd claim for any checklist.

The honest limit

The oracles catch implementation bugs. They can't catch specification bugs, because I generated them from the same specification.

One of them was stronger by accident. The missed-opportunity oracle used brute-force evaluation of every day in a period, while the implementation used a boundary scan that only evaluates the dates where state can change. Two genuinely different algorithms agreeing is better evidence than one algorithm agreeing with itself, for the same reason a winemaker checks a refractometer against a hydrometer: instruments that fail differently are unlikely to fail together. I wrote about that idea recently, too. But even that oracle shares the specification's assumptions about what a missed opportunity is.

Tests tell you the implementation disagrees with your expectations. They can't tell you whether your expectations deserve to win.

What I'd keep

Four habits, one per lie:

Give every inertness test a positive control. If it asserts that something has no effect, prove the effect exists when it should.

Give every alarm a negative control. If it can't be shown passing on known-clean input, it isn't a check. It's noise with a red icon.

Test properties across the domain, not just the examples. Your examples are the paths you already know work, and the contract is only as good as the cases you thought to ask about.

Verify the target before the result. Make the system under test visible on the screen and in the diff, so "which one am I looking at?" is never a guess.

None of those habits is about AI, and that's the point. The agent made every one of these lies cheaper to commit and faster to find. It didn't change which checks were honest.

The rest is the usual advice, and it wasn't what saved me.


This came out of building The Cellar, an event-sourced wine collection for the Sanity DEV Challenge. The full build is documented in the friction logs in that repository, including every check above and the audits that found them.

Top comments (70)

Collapse
 
alexshev profile image
Alex Shev •

The useful distinction here is between a test that proves a behavior and a test that merely agrees with its own fixture. For AI-assisted changes I now want an independent observable too: a changed input, a read-back from the real boundary, or a negative case that would fail if the implementation were only cosmetically correct.

Collapse
 
baigorria profile image
Guillermo Baigorria •

One more way a test can lie, when the thing you test is an AI model: the same version, run twice with nothing changed, doesn't always give the same results.

So before I trust that a prompt change helped, I run the old version twice and count how many cases changed on their own. That's my noise floor. If the new version changes fewer cases than that, the "improvement" may just be chance.

It's your negative control, applied to a model: a scale that drifts a pound on its own can't tell you that you lost half a pound.

Collapse
 
rawveg profile image
Tim Green •

Respectfully, that isn’t a test lying. Variance between runs is the model doing exactly what a probabilistic system does. The problem is pointing a deterministic test at a probabilistic outcome and then blaming the result. Anyone who’s written tests against timestamps knows how that ends: not if it fails, but when.

Your noise floor is a sensible workaround, but notice what it concedes: a single run-and-compare was never a valid test in the first place. And that’s the real issue. This isn’t one test; it’s several, and they answer different questions.

  1. Is the model responding at all? Deterministic. You don’t care what comes back, only that something does, within your timeout and on your terms.

  2. Are you handling the response correctly? Also deterministic. The model can return literally anything at any time: malformed output, the wrong schema, a refusal, an empty string, a novel. Your code must reach a well-bounded, predictable state every time. Validation, exception handling, fallbacks. That’s where your system’s reliability actually lives.

  3. Is the output any good? That’s evaluation, not testing, and it’s where statistical methods like your noise floor belong, ideally with far more than just two runs.

The test isn’t lying to you. You’re lying to yourself if you think layer three, on its own, is your testing strategy.

Collapse
 
baigorria profile image
Guillermo Baigorria •

Fair point, the variance is just the model, and two runs can miss it.

How many runs you need depends on how small a change you want to see. Running everything that often gets expensive while you iterate on a prompt, though.

A cheaper way: run each version twice, and keep the history. When the two runs of the same version disagree, mark that case as unstable. A change in that case needs more runs before it counts. The extra runs go only where the noise is.

Thread Thread
 
rawveg profile image
Tim Green •

That’s a smart refinement: spend the extra runs where the noise actually is, rather than everywhere. It also fits neatly into a proper evaluation harness, since you’re logging that history anyway.

One point to be aware of though is that two agreeing runs don’t prove a case is stable. A case that flips 30% of the time will still agree with itself in roughly six out of ten pairs, so plenty of noisy cases will be marked stable on the first pass. Keeping the history across iterations helps, because they’ll eventually show their hand. I’d also pin the seed (where your provider supports it) and the model version first, so you’re measuring the variance you can’t remove rather than the variance you can and over time the evaluation becomes more stable.

Cheap where it can be, rigorous where it needs to be. That’s a good balance.

Collapse
 
nullandvoid_ profile image
Jyanthi •

This is a great reminder that having more tests doesn't automatically mean having better testing. I especially liked the point about tests passing for the wrong reasons. Verification needs to challenge assumptions, not just confirm that the code works as expected. That's even more important when AI is writing the code and the tests!

Collapse
 
hellomathieup profile image
Mathieu Poli •

Great piece.
"The agent broke a rule and told me" is such an underrated result: honest reporting turns out to be worth more than perfect obedience.

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The paired-assertion rule is the one I'd steal — a test that X has no effect means nothing without the companion proving the effect exists when X is enabled. Your mutation check (delete the clause, watch it finally fail) is the honest version of coverage. Do you run that check across the whole suite now, or only on the tests you already suspect?

Collapse
 
kenwalger profile image
Ken W Alger •

Targeted, not suite-wide. I haven't automated mutation testing across the full suite, and the checks I describe were performed manually during the audit.

I started with tests that looked suspicious, but I wouldn't want suspicion to be the permanent selection criterion. Some of the most dangerous vacuous tests look perfectly ordinary until you deliberately remove the behavior they're supposed to protect.

My longer-term preference would be to require positive controls and targeted mutation checks for load-bearing invariants, particularly authorization, state integrity, and irreversible operations. That gives the practice a defined scope without pretending every assertion needs the same level of scrutiny.

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That scope is the honest middle — requiring positive controls plus mutation checks only where the invariant is load-bearing, rather than pretending every assertion deserves the same scrutiny. The selection problem you flag is the interesting one though: if suspicion can't be the permanent criterion, what replaces it — is it the invariant's blast radius, or do you rotate the target set so the ordinary-looking tests get their turn too?

Thread Thread
 
kenwalger profile image
Ken W Alger •

I'd use both, but for different purposes. Blast radius would determine which tests get mandatory positive controls and mutation checks. Anything protecting authorization, data integrity, irreversible operations, or a central domain invariant should qualify regardless of whether the test looks suspicious.

But that still leaves the selection problem you identified. We don't necessarily know which ordinary-looking tests are protecting assumptions that turn out to be important. I'd use a rotating sample of the remaining suite, deliberately including tests that have been green for a long time and haven't attracted much attention. When one fails a mutation check, I'd look for other tests built around the same assumption or fixture pattern rather than treating it as an isolated defect.

I haven't implemented that rotation in Cellar, so this is how I'd approach scaling the practice rather than something I've already validated. The original audit taught me that suspicion is useful for finding the first problem, but it's not a reliable coverage strategy.

The goal wouldn't be to mutation-test everything eventually. It would be to make the selection criteria explicit and use what the sampled tests reveal to improve them.

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

Using both is the honest answer — blast radius for the mandatory set, rotation for everything else. The part I'd underline is treating a failed mutation check as a signal to audit the fixture pattern, not just the one test: that's where a single vacuous test becomes a systemic find. Do you rotate on a schedule, or does a change to a shared fixture trigger its dependents' turn?

Thread Thread
 
kenwalger profile image
Ken W Alger •

I'd favor a change to a shared fixture triggering a review of its dependents, with periodic sampling to catch problems that don't announce themselves through a change. But that's a proposed approach, not something I've implemented in Cellar.

The larger lesson from this audit was that one vacuous test can reveal a shared assumption affecting several others. That's the part I'll carry forward into future projects.

Thanks for the thoughtful discussion!

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That shared-assumption lesson is the part worth carrying forward — a vacuous test is rarely alone, since the fixture pattern behind it usually feeds several others. And the honesty about it being a proposed approach, not yet implemented, is exactly what makes it usable: it reads as a direction, not a claim. Good luck with the next audit — if the rotation ever surfaces a long-green test that fails its mutation check, I'd be curious what assumption it was quietly protecting.

Collapse
 
heyvaldemar profile image
Vladimir Mikhalev •

A fifth one from container scanning: the gate that cannot tell "nothing critical" from "nothing read".

trivy image --exit-code 1 --severity CRITICAL busybox:1.37 exits 0 with no warning. Trivy detects no OS in that image, so it scans no packages, and the report has no Results at all. The signed cosign-vuln attestation carries the same absence, and it reads as clean. A positive control catches it only if the control image is one the scanner can read: alpine:3.10 fails on CVE-2021-36159.

The clause I added is your "not verified" outcome written into the admission policy: no severity is counted until the report has a Results entry of class os-pkgs. An empty scan is a missing scan. I reported the exit code upstream: trivy discussion #11403

Collapse
 
kenwalger profile image
Ken W Alger •

That's an excellent fifth example. The distinction between "no critical vulnerabilities found" and "no OS packages examined" is exactly the kind of evidence boundary I was trying to get at.

What's particularly interesting is that signing the attestation doesn't resolve the problem. It can establish the provenance and integrity of the report without establishing that the report actually covered the intended material.

I like your admission-policy requirement for an os-pkgs result. It makes successful coverage a prerequisite for interpreting the severity findings, rather than allowing an empty result to masquerade as a clean one.

Thanks for sharing the concrete reproduction and upstream discussion. This is a particularly useful example of why verification has to establish what was examined before interpreting what was found.

Collapse
 
heyvaldemar profile image
Vladimir Mikhalev •

Your split is the one I would keep: the signature says who produced the report and that nobody changed it since, and nothing about what it read. A Trivy maintainer answered the discussion on 27 October: no warning and no fail-on-empty are planned, since an image with no detected OS is expected for many targets, a scratch image with one binary among them. The one trace of the miss is the line OS is not detected., printed under --debug only.

So the coverage check stays on the consumer's side, written into the policy as a required os-pkgs result. The scanner will not say it for you.

Collapse
 
alexshev profile image
Alex Shev •

The proposed-claim fixture is a great example of a test passing for the wrong reason. A useful companion is to mutate the review-state filter and require the test to fail; that checks the test can actually detect the rule it claims to protect.

Collapse
 
kenwalger profile image
Ken W Alger •

Exactly, and that's what finally gave me confidence in the repaired test. Removing the review-state filter had to make the test fail. Before the repair, it stayed green because the fixture's recency ordering masked the missing rule.

I ended up finding two more tests with the same underlying weakness, and all three repairs were verified by deliberately removing the logic they claimed to protect. It's a simple experiment, but it changes the question from "does this test pass?" to "can this test actually detect the failure I care about?"

Collapse
 
alexshev profile image
Alex Shev •

That is exactly the test I trust most: deliberately break the protected rule and watch the test fail for the expected reason. It is a small mutation test, but it catches fixtures that accidentally make a green result meaningless.

Collapse
 
tomzafred profile image
Tom Zafred • • Edited

The fourth lie - the right test executing against the wrong system because an environment variable silently defaulted to production - is the most insidious failure mode in autonomous workflows, because every assertion passes with total internal consistency.

I ran into this exact boundary in automated test runners. If an agent writes to staging while the application bundle silently falls back to a default production connection, no amount of assertion rigour will catch it. The traditional remedy of inspecting headers or diffing literals still relies on human vigilance.

In my own test harness, the countermeasure that proved robust is decoupling test certification from raw process exit codes and binding runs to an explicit runtime target fingerprint:

# Extract authoritative catalogue from the active runtime connection
ACTIVE_CATALOG=$(node -e "import('./db.js').then(m => m.getCatalogName()).then(console.log)")

if [ "$ACTIVE_CATALOG" != "$EXPECTED_TEST_CATALOG" ]; then
  echo "TARGET_MISMATCH: Test suite connected to $ACTIVE_CATALOG, expected $EXPECTED_TEST_CATALOG" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

By embedding this target fingerprint directly into a signed execution receipt before evaluating assertions, any silent fallback triggers an immediate abort before a single write or read executes.

To address the first lie (the test that cannot fail) and the agentic tendency to silently tamper with oracles to turn a red suite green, two complementary constraints proved essential:

  1. An immutable filesystem fence: stripping the agent of write access to tests/ and oracle fixtures during the code synthesis phase. Pairing this with a pre-flight working-tree hash immediately catches whether the agent relaxed an assertion or deleted an edge case to force a pass.
  2. An assertion-count floor: tracking empirical assertion execution counts rather than binary exit 0 statuses. If an agent comments out an assertion or swallows an error in a try-catch block, the runner flags a vacuous test run and fails the build.

Your wine refractometer versus hydrometer analogy hits the core: verification only holds when the observer and the observed fail through completely independent mechanics.

Collapse
 
kenwalger profile image
Ken W Alger •

The runtime target fingerprint is a great improvement over my hand-edited literal. Querying the active connection and comparing it against the expected target checks where the application actually connected, not where its configuration claimed it would.

I also like separating the agent's ability to modify implementation code from its ability to modify the tests and oracles that judge that implementation. That's a meaningful independence boundary.

I'd add one qualification to the assertion-count floor: it's useful for detecting missing execution, but the original proposed-claim test demonstrated that an assertion can execute successfully and still prove nothing about its intended rule.

Taken together, these controls address different failure modes: target identity, oracle integrity, execution coverage, and behavioral validity. None replaces the others. Thanks for the detailed example!

Collapse
 
tomzafred profile image
Tom Zafred •

An assertion count only confirms that a check executed, not that it had any capacity to fail.

A test can run six assertions and increment the counter, yet prove nothing if the oracle is tautologically bound to the implementation. When an agent (or a fatigued engineer) writes an ineffective test, the failure mode is rarely an omitted assertion; it is circular derivation. The assertion compares an output not against an immutable external fixture, but against an intermediate value derived from the very logic under evaluation. Both sides of the assertion drift together, exit code 0 is minted, and the regression passes undetected.

In my biobanking systems, where clinical data integrity standards treat unverified test claims as regulatory violations, I borrowed a concept from analytical chemistry to catch this without running expensive full-suite mutation testing: the automated negative control probe.

Every load-bearing assertion is paired with a corrupted input vector during the exact same execution turn:

// Baseline positive assertion:
assert.strictEqual(parseSettlement(validBatch).status, "RECONCILED");

// Paired negative control: MUST throw or the oracle is a placebo
assert.throws(() => parseSettlement({ ...validBatch, checksum: 0xDEAD }), /ChecksumMismatch/);
Enter fullscreen mode Exit fullscreen mode

If the negative probe fails to throw when fed an input with a poisoned checksum or an inverted timestamp, the oracle has zero sensitivity. The runner rejects the test as a vacuous tautology before the build can advance.

Your distinction between target identity, oracle integrity, execution coverage, and behavioural validity is the cleanest taxonomy of this problem space I have read. Each sits at a completely different architectural layer.

Thread Thread
 
kenwalger profile image
Ken W Alger •

That's a useful distinction, especially the difference between an assertion executing and an assertion demonstrating sensitivity to a known defect.

I like the paired negative probe because it makes that sensitivity observable during the normal test run rather than requiring a separate mutation exercise. It also gives the test a much clearer claim: this particular corrupted input must be rejected.

I'd preserve one boundary, though. Demonstrating that a poisoned checksum is detected doesn't necessarily establish that the oracle is independent or that other failure modes would be caught. It establishes something narrower, but importantly, something we can actually verify.

And I think that's where we keep arriving in this discussion: the value of evidence depends on being precise about what it establishes, not simply whether the machinery produced a successful result.

Thanks for bringing the biobanking perspective into this. It's a fascinating application of the same principles.

Collapse
 
friga profile image
Artur Pokładek •

Your first one has a cousin I keep finding on client sites: an accessibility audit that's green because every image has alt="image". The check asks whether the attribute exists, and nothing in the pipeline ever reads what it says.

Do you have a control for that kind of check, or is that simply the point where a human has to look?

Collapse
 
kenwalger profile image
Ken W Alger •

That's a great example of a check answering a much narrower question than the one we actually care about. The pipeline proves that an attribute exists, not that someone using a screen reader gets useful information from it.

I'd approach it with two layers. Automated checks can reject empty or generic values where descriptive alt text is expected, flag repeated descriptions, and identify suspicious patterns. But those are still heuristics. A human needs to assess whether the description communicates the image's purpose in context, including whether the image is decorative and should have empty alt text.

I'd want a known-bad control example, such as a meaningful image with alt="image", that the audit must reject. That at least proves the check can detect the failure it's supposed to catch. It doesn't eliminate the need for human review, but it makes the automated part more defensible.

Collapse
 
friga profile image
Artur Pokładek •

A known-bad control is such a cheap fix too. One alt="image" left on a staging page on purpose, and the audit has to go red every time, or the audit is the thing that's broken.

Collapse
 
sameerqaiser17 profile image
Sameer Qaiser •

The line that hit me: "When one tool writes an implementation and its tests from the same prompt, they share one set of assumptions, and a wrong assumption gets encoded twice and then agrees with itself."

This is terrifyingly accurate, and I just experienced a version of this in technical writing.

I wrote a 1,500-word Python tutorial using a very logical, structured template. I checked it myself, ran the code, and all my "tests" passed. I thought it was perfect. Then an editor at freeCodeCamp read it and told me it read too much like AI and needed to be completely rewritten.

My verification was broken because I was grading my own homework. The editor was the independent oracle.

Your point about "the right test against the wrong system" also hits hard. It's so easy to verify that the code runs, without verifying that the code actually solves the right problem. This is a brilliant breakdown of how dangerous a "green" check can be if it shares the same blind spots as the implementation. Saved!

Collapse
 
kenwalger profile image
Ken W Alger •

That's a useful parallel, particularly because running the code and evaluating the tutorial are two different verification tasks. The code can execute correctly while the article still fails to meet the editorial expectations of its audience or publication.

An independent editor brings a perspective that the author, or an AI working from the author's instructions, may not have. It's not quite the same oracle as a software test, but the underlying problem is familiar: we can verify the things we've thought to measure and still miss the requirement that actually matters.

Thanks for sharing the experience, and for reading!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.