Plant the Faults So the Harness Proves It Can See Them
A harness that has never detected a known fault has not been shown to detect anything. Injecting deliberate, known faults is the step that converts a green suite into a suite that can fail for the right reasons. This covers the fault classes worth planting in an extraction or retrieval pipeline, how each one should present, and why a planted fault is the positive half of a pair whose other half is the negative control.
Plant the Faults So the Harness Proves It Can See Them
The short answer. A test harness that has only ever run against a working system tells you one thing: that it did not complain. It does not tell you it would have complained. Those are different properties, and only one of them is useful. The way you get the second is to break the system deliberately, in a way you fully control, and confirm the harness goes red and names the right stage. A fault you planted and detected is evidence. A green run on healthy input is a decoration. For a pipeline with extraction, retrieval, fusion and generation stages, each stage has its own short list of faults worth planting.
I have watched a team spend a fortnight hardening a retrieval pipeline against an evaluation harness that could not have failed. Every assertion was on the final answer, every assertion was loose enough to pass whatever came out, and nobody had ever seen it go red.
The harness was not wrong. It was unproven, which from a decision-making point of view is the same thing.
Passing is a property of the test, not of the system
A check that returns green is reporting on itself. It ran, and it did not find the condition it looks for. Whether it is capable of finding that condition is a separate question that a green run cannot answer.
There are only two reasons a check passes. The system is correct, or the check cannot see the fault. From the outside those look identical. The only way to separate them is to introduce the fault on purpose and watch what happens.
This is why injected faults are not an optional extra on top of a test suite. They are what makes the suite into evidence. Before the first planted fault is detected, you have a script that runs. After it, you have a measurement instrument with a known response.
The framing I find most useful is to treat every check as having a sensitivity that must be demonstrated rather than assumed. You would not trust a smoke detector you had never tested. A regression suite guarding a pipeline is in exactly the same position, with the difference that nobody sells a test button for it.
The fault classes worth planting in an extraction pipeline
These are the ones that have earned their place, in the sense that each corresponds to a real failure I have seen reach production and go unnoticed.
Field swap. Take two fields of the same type and exchange them in the output. Two dates, two monetary amounts, two party names. This is the single most valuable fault to plant, because a schema check will not see it, a type check will not see it, and the most common real extraction error is exactly this shape. If your harness cannot detect a swapped pair, it cannot detect the dominant error class in your system.
Off-by-one row or column. Shift a table read by one position. Take the subtotal instead of the total, the prior period instead of the current one. Again, plausible values in the right format, which is precisely why it survives.
Silent truncation. Cut a long field at a boundary. An address that loses its last line, a name that loses a suffix, a description cut at a page break. The output remains valid and remains wrong.
Unit or scale change. Multiply an amount by a thousand, or change a currency without changing the number. If nothing downstream has a range assertion, this passes.
Empty with a plausible default. Replace a missing value with zero, or with the modal value of the field. This is the fault that most often exists for real, because pipelines are frequently written to fill gaps rather than report them.
Encoding corruption. Push a non-ASCII character through a lossy step. Accented names and non-Latin scripts are where this lives, and it is usually invisible on the test corpus because the test corpus is English.
The fault classes worth planting in a retrieval pipeline
Retrieval has its own set, and they are different in kind because the failure is usually an absence rather than a wrong value.
Dead branch. Disable one retrieval branch entirely and have it return an empty list. If you run a dense branch and a lexical branch, kill the lexical one. A fused pipeline will frequently produce an answer that looks fine, because the surviving branch covers the easy queries. The harness should name the dead branch, not just report a lower score.
Starved candidate list. Reduce the number of candidates a branch returns to a fraction of what the next stage expects. Not zero, just thin. This is the fault that produces the thin-answer symptom and it is the hardest to see from the output.
Shuffled ranking. Keep the same candidate set and randomise the order. If your harness scores only on whether the right document appeared anywhere in the set, this passes, and you have just learned that your harness does not measure ranking.
Index staleness. Serve retrieval from a deliberately out-of-date index while the gold set reflects current content. This is the production failure that evaluation harnesses almost never model, because evaluation usually rebuilds the index as part of the run.
Filter inversion. Flip the sense of a scope filter so the wrong tenant, document type or date range is returned. The answers will be fluent and sourced from the wrong place, which is the worst possible combination.
Context substitution. Give the generation stage context from a different query. If the answer still scores well, your scoring is reading fluency rather than groundedness, and that is worth knowing before you ship.
Planted faults and negative controls are two halves of one pair
The relationship confuses people, so it is worth being precise.
A negative control is an input you know is bad, run through the system to confirm the system rejects it. An unanswerable question that should produce an abstention. A query about a tenant with no matching content that should return nothing rather than something adjacent. A document in a format you do not support that should be routed to review rather than guessed at. The system under test is intact and the input is the known-bad thing.
A planted fault is the inverse. The input is ordinary, the system is deliberately broken in a known place, and what you are testing is the harness rather than the system.
Put them together and you get the two directions of evidence any instrument needs. The negative control shows the system refuses what it should refuse. The planted fault shows the harness notices when the system stops refusing. Without the second, a negative control that passes is itself unproven, because a control that always passes looks the same as a control that cannot fail.
In practice I run them as one exercise. Build the negative control set, then plant a fault that should make a specific negative control fail, and confirm it does. That single loop is what turns both of them into evidence.
Making it a habit rather than a project
The reason this discipline rarely survives contact with a schedule is that it gets framed as a one-off audit. Framed that way it is always the thing that gets cut.
A few practices that make it stick:
Keep the faults as code, not as a document. A fault should be a switch you can turn on in the pipeline configuration, named for what it breaks. Dead lexical branch. Swapped date pair. Stale index. If it exists only as a written procedure, it will be run once.
Run the fault suite on a schedule, not on demand. Nightly or weekly, with the expectation that every planted fault is detected and attributed. A planted fault that stops being detected is a regression in your evaluation, which is more serious than a regression in the pipeline, and nothing else will tell you it happened.
Require attribution, not just detection. A harness that goes red for every fault but always points at the same stage has not localised anything. The assertion should be that fault X produces a failure naming stage X. That requirement is what forces per-stage instrumentation to exist in the first place.
Add a fault every time you find a real bug. When something escapes to production, the fix is two commits. One repairs the pipeline. The other plants that fault permanently, so the harness is demonstrably able to catch its recurrence. Over a year this builds a fault set shaped by your actual failure modes rather than by someone's imagination.
The honest limit
Planted faults prove sensitivity to the faults you planted. They say nothing about the ones you did not think of, and the faults nobody thinks of are, by definition, where the serious incidents come from. A fault suite is a lower bound on harness capability, never a certificate.
There is also a coupling assumption underneath the whole practice. Detecting an artificial fault is taken as a proxy for detecting real ones of the same class, and that proxy is imperfect. Published work on mutation testing has looked directly at how well artificial faults stand in for real ones and found the relationship real but incomplete, with a meaningful share of real faults coupling to no artificial one even in principle. Treat the proxy as useful and not as equivalence.
And there is a genuine cost. A fault-injection switch is a code path in your pipeline that exists only for testing, which is a small ongoing maintenance burden and a small risk surface if it can ever be enabled by accident. Gate it behind a build flag, keep it out of production configuration entirely, and accept that you are trading a little complexity for the ability to believe your own test results.
FAQ
Why isn't a passing test suite enough evidence? Because passing is a property of the test. A check returns green either because the system is correct or because the check cannot see the fault, and those are indistinguishable from outside. Until a known fault has been introduced and the harness has gone red and named the right stage, the suite has demonstrated only that it runs without complaining.
What is the single most useful fault to plant in an extraction pipeline? Swapping two fields of the same type, such as two dates or two monetary amounts. Schema validation and type checks both pass, the output looks entirely plausible, and this is the shape of the most common real extraction error. A harness that cannot detect a swapped pair cannot detect the dominant error class in the system it guards.
How is a planted fault different from a negative control? A negative control feeds known-bad input through an intact system to confirm it is rejected. A planted fault feeds ordinary input through a deliberately broken system to confirm the harness notices. The first tests the system, the second tests the harness, and a negative control that has never been made to fail is itself unproven.
Should the harness only detect the fault, or identify it? Identify it. A harness that goes red for every planted fault but always blames the same stage has detected something and localised nothing. Asserting that each planted fault produces a failure naming its own stage is what forces per-stage instrumentation to exist, and attribution is the part that saves time during a real incident.
How do I keep fault injection from becoming a one-off audit? Make each fault a named configuration switch in the pipeline rather than a written procedure, run the whole set on a schedule, and add a new fault every time a real bug escapes. A planted fault that silently stops being detected is a regression in your evaluation, and a scheduled run is the only thing that will tell you.
Do artificial faults really stand in for real ones? Partly. Published work in mutation testing has examined the question directly and found that artificial fault detection does track real fault detection, more strongly than code coverage does, while a meaningful share of real faults couple to no artificial fault at all. So a fault suite is a lower bound on what your harness can see, and never a guarantee of completeness.
Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.
Bring us the program