Test-Enforced Redaction Claims — Seed the Secret, Assert Its Absence¶
Pattern¶
Any written guarantee of the form "we strip / redact / never emit X" must be backed by a test that (1) seeds a fixture containing X and (2) asserts X is absent from the output. A redaction guarantee expressed only in prose — or only in a denylist the tests never exercise — drifts from the code the moment either side changes, and nothing fails.
The positive-only test ("the numbers we do want are present") is not enough: it proves the happy path, not the guarantee. The guarantee is a negative property, so it needs a negative assertion against a fixture that would trip it.
When this bites¶
The trap fires whenever all three hold:
- A privacy/security claim promises some value class is removed (PII, secrets, identifying attributes, tokens, hostnames).
- The mechanism is a denylist (
_IDENTIFYING = {...}, a regex set, a field allowlist) — easy to fall out of sync with the documented set. - The tests assert only that wanted data survives, never that a seeded member of the redacted class is gone.
Example: SPEC-129 (H1 / SEC-1)¶
configs/monitoring/otel-receiver.py documented — in docs/operations/monitoring.md
and the patch README — that identifying attributes including terminal.type
were stripped before anything is written to metrics.jsonl. But the
_IDENTIFYING denylist omitted terminal.type, so it flowed straight through
normalize() into the persisted file. The OTLP fixture never contained
terminal.type, and the sanitize test only grepped for the four keys that were
in the set — so 19/19 tests passed while the written privacy guarantee was false.
Three reviewers converged on it (security-sentinel, kieran-python, document-quality) in a follow-up pass — exactly the "reviewer finds it manually after green tests" symptom.
The fix had two halves, and both are load-bearing:
- Close the code gap — add
terminal.typeto_IDENTIFYING. - Make the guarantee testable — seed the fixture with
terminal.type/iTerm.appand extend the sanitize assertion to prove it is absent frommetrics.jsonl:
# fixture now carries the value the docs promise to strip
{"key": "terminal.type", "value": {"stringValue": "iTerm.app"}}
# assertion fails if ANY documented-stripped key survives
! grep -qE "user\.email|account_uuid|organization\.id|session\.id|terminal\.type|iTerm" "$metrics_out"
Half 1 alone fixes today's bug; half 2 is what stops it regressing when someone edits the denylist or the docs next.
How to apply¶
- For every "we strip/redact/never emit X" sentence, ask: which test seeds an X and asserts it's gone? If none, that sentence is unverified.
- Seed the fixture with a distinctive sentinel for the redacted value
(
iTerm.app,SECRETTOKEN_...) so a leak is unambiguous in output. - Keep the assertion's key list in lockstep with the documented set — when the docs list N identifying attributes, the negative grep should cover all N.
- Prefer this negative test to live at the review gate, since redaction drift is a classic finding that survives happy-path suites (see structural-test-assertions.md for the sibling "presence ≠ correctness" trap).
Signal¶
A denylist named after a security/privacy property (_IDENTIFYING, _SECRETS,
REDACT) with no fixture that contains a member of that set is a smell. The
guarantee is only as strong as the seeded negative test behind it.