Skip to content

Design: falsifiable service test plans — a golden path that cannot fail is not a test

The Service Test Plan Standard (ADR-0102) mandates that every service owns a golden-path test, but says nothing about whether that test is capable of failing. At least eight plans assert on a version endpoint or a health ping, and tandoor’s was proven blind on 2026-08-14 — the production image serves both its readiness and golden-path assertions with no database reachable at all. That gate drives auto-rollback on a family-facing service. This design adds a falsifiability clause to the standard and two mechanisms that make an untestable test impossible to ship and prove the rest rather than assume them.

The problem: the standard governs whether a test exists, not whether it can fail

Section titled “The problem: the standard governs whether a test exists, not whether it can fail”

The conformance floor is “at least one functional (behavioural) test, beyond readiness/smoke”. That wording is satisfied by any assertion that is behavioural in form, regardless of whether it discriminates. The result, measured across the 34 docker-stacks plans on 2026-08-14:

ClassCountExamples
Genuinely strong — asserts on real data or a real transformation~20qdrant (canary write→read round-trip), paperless, netbox, freescout, topcharts, vectormap
Blind — assertion survives a dead backend8tandoor, incident-webhook, ombi01, sab01, rustdesk01, dtrack, pingvin, portainer
Stateless — correct as authored, no persistence layer to touch5gotenberg (HTML→PDF), tika (text extraction), smtp (EHLO), docs, cloudflared

The tandoor proof is the worked example. Running vabene1111/recipes:2.6.13 with prod-faithful environment and --network none, so no database is reachable:

Plan stepResult with the database dead
readiness — GET /HTTP 200 — passes
golden_path — GET /openapi/HTTP 200, body carries Tandoor API Docs — passes

Both steps go green with Postgres gone. This is failure class FC-02 assertion-cannot-fail: a check that would still pass with the feature removed. It also invalidates the reasoning in ADR-0011, which assumed tandoor’s readiness probe catches its real failure mode — GET / likewise returns 200 with the database gone.

The discipline already existed one layer down, and was never inherited. ADR-0011’s own Consequences section requires that probe depth be proven: “depth must be verified (induce a dep failure, confirm probe_success→0) before treating a path as more than readiness”. That rule was applied to blackbox probes and never carried into the Service Test Plan Standard, so the same class of unverified-depth assertion reappeared at the acceptance-gate layer. This design is largely that existing rule, promoted to where it was missing.

Decision tree — the resolved architecture

Section titled “Decision tree — the resolved architecture”
chosenrejectedchosenrejectedchosenrejectedrejectedchosenchosenin delivery / hypercarepast hypercareGolden path can passwith backend deadClause form?Falsifiabilityclass-agnosticPersistence-layer clausebreaks 5 stateless servicesWhat enforces it?Shape gate at push+ periodic red-proofAuthoring review only= the regime that produced8 blind plansSever method?Ephemeral clonefrom rendered composeSynthetic-null replaywould MISS tandoorSever real dependencybreaks production on ascheduleReal secrets via env-file--network none, --rmThree-way verdictwon't boot = conformant(hard dependency)assertion passes = BLINDassertion fails = conformantMetric plans?Convert 6 thin proxieskeep 4, fix over_timewindowsWhen does it run?On plan change in CI+ monthly full sweepFreshness dead-manon the sweepFinding routes where?Read service_lifecycle.ymlSnag on that EpicFreeScout incident
chosenrejectedchosenrejectedchosenrejectedrejectedchosenchosenin delivery / hypercarepast hypercareGolden path can passwith backend deadClause form?Falsifiabilityclass-agnosticPersistence-layer clausebreaks 5 stateless servicesWhat enforces it?Shape gate at push+ periodic red-proofAuthoring review only= the regime that produced8 blind plansSever method?Ephemeral clonefrom rendered composeSynthetic-null replaywould MISS tandoorSever real dependencybreaks production on ascheduleReal secrets via env-file--network none, --rmThree-way verdictwon't boot = conformant(hard dependency)assertion passes = BLINDassertion fails = conformantMetric plans?Convert 6 thin proxieskeep 4, fix over_timewindowsWhen does it run?On plan change in CI+ monthly full sweepFreshness dead-manon the sweepFinding routes where?Read service_lifecycle.ymlSnag on that EpicFreeScout incident
DecisionChoice madeRationaleRejected alternatives
Clause formFalsifiability, class-agnostic: the assertion MUST fail when the service’s primary dependency is unavailableCatches tandoor without making the five stateless services non-conformantA literal persistence-layer clause (breaks gotenberg/tika/smtp/docs/cloudflared); a per-class floor matrix (heavier, needs every service classified first)
EnforcementPush-time shape gate and periodic red-proofAuthoring review alone is precisely the regime that produced the eight blind plansReview only; chaos red-proof on every deploy (doubles gate runtime, new flakiness in the deploy path)
Sever methodEphemeral clone booted from the rendered compose with dependencies severedThe method that actually caught tandoorSynthetic-null assertion replay — would have passed tandoor, because its assertion does discriminate against an empty body while the service serves a full one; severing the real dependency in a maintenance window — deliberately breaks production, including family-facing services
Harness shapeOne generic harness deriving image and env from the compose templateNo per-service fixtures to drift out of step — fixture drift is the same bug class being fixed34 committed per-service fixtures; scaling real deps to zero (port and volume collisions with production on docker01)
Metric plansConvert the 6 thin proxies; retain 4 and fix their *_over_time windowsBlanket conversion would destroy maintainerr’s seven temporal invariants, and count(up == 1) is the artifact for Prometheus itselfConvert all ten; keep all ten unchanged (stale-tolerant windows defeat the red-proof)
SecretsReal rendered secrets, passed by env-file, --network none, --rmDummy secrets make the service fail to boot for the wrong reason, manufacturing a conformant verdict that was never earnedDummy placeholders; non-secret env only; real secrets for authenticated plans only
CadenceRed-proof on plan change in CI, plus a monthly full sweepA newly authored blind plan is caught at birth; drift in an unchanged plan within 30 daysWeekly full sweep (a blind plan still ships for up to a week); weekly sample of 3–5 (a blind gate could drive auto-rollback for a quarter)
Findings routingBy service lifecycle: in delivery or hypercare → snag on that Epic; past hypercare → FreeScout incidentArron’s house pattern — it makes “accepted into PROD” mean something precise rather than a judgement callAll findings as snags (invisible until an Epic closes); Pushover only (no record, no owner)
Lifecycle sourceNew ansible/scripts/doc_gen/service_lifecycle.yml overlayService pages are auto-generated from the Alloy probe inventory (ADR-0059) — a field written onto the page is destroyed at the next regen. doc_gen/ is the established home for declarative per-service metadataA field on the generated page (overwritten); querying ADO for an Epic by description text (FC-11 guessed-identifier); a hand-maintained exception list
Script homedocker-stacks/scripts/Follows the ADR-0105 synthetic-gate precedent — a routine-driven gate living beside the stacks it checksansible/scripts//usr/local/bin (pins the gate to one provisioned agent)
Doctrine migrationDeferred to its own grillMigrating /grillme’s embedded doctrine into a standard touches the skill, a new standard, /wrapup reconciliation and every future service build — it deserves its own gateFolding it into this Epic (doubles an already three-workstream batch, and the migration itself would ship ungrilled)

The three-way verdict is what makes the red-proof attributable

Section titled “The three-way verdict is what makes the red-proof attributable”

A red-proof that only asks “did the assertion fail?” is satisfied by a clone that never started — which is the same self-deception one level up. Because the harness supplies the real rendered environment, the only thing missing from the clone is the dependency, so each outcome is attributable:

Clone outcomeVerdictMeaning
Container will not bootConformantThe service hard-depends on its dependency; any assertion is necessarily falsifiable
Boots, golden path passesBLIND — non-conformantThe assertion cannot distinguish a working service from a hollow one
Boots, golden path failsConformantThe assertion genuinely discriminates

This is why dummy secrets are rejected: they collapse row 1 and row 3 into an indistinguishable “failed for some reason”, and the harness would record a conformance it never established.

RequirementHow this design satisfies it
MonitoringThe monthly sweep carries a freshness dead-man — a sweep that silently stops running is otherwise indistinguishable from a sweep finding nothing (FC-03 blind-watcher).
ObservabilitySweep emits per-service verdicts and a run summary; findings carry the service, the assertion and the clone outcome. Logs ship to Loki per the Logging Standard.
AlertingDead-man alert on sweep staleness. Findings route by lifecycle rather than paging — a blind plan is a defect record, not an interrupt.
SBOM / vuln coverageNo new long-lived image. The harness runs the pinned images already tracked in Dependency-Track; clones are --rm and ephemeral.
Docs as codeThis design doc, the standard amendment, the governing ADR, and an operations runbook for the sweep — all in the same change as the code.
Config as codeGate and harness in docker-stacks/scripts/; lifecycle overlay in ansible/scripts/doc_gen/; sweep schedule declared in schedule_config.yml. Nothing manual-only.
SecurityReal credentials are resolved as the deploy resolves them, passed by env-file — never argv (/proc/<pid>/cmdline is world-readable on a host five sessions share), into a container with --network none (no egress path) and --rm (seconds of lifetime). Harness output is scrubbed before any finding is recorded.
Update & patch lifecycleFits the GitOps pinned-tag house pattern: the harness scans whatever tag group_vars currently pins, so it follows the fleet automatically with no separate refresh path. The gate and harness scripts are ordinary repo content, updated by commit; their staleness signal is the dead-man plus the on-change trigger, which exercises the harness every time any plan changes. No pinned-forever component is introduced.
Planned maintenanceThe design deliberately never touches production — the clone is isolated — so no silence is required to run it. The one-off fleet remediation redeploys services and follows the normal mute-first discipline.
NotificationsNo new Pushover path. Findings become snags or incidents; the dead-man uses the existing alerting route at priority 0, within quiet hours.
TaggingFindings carry service, domain and verdict labels conforming to the Tagging Standard.
Enterprise patternThis is mutation testing applied to acceptance gates — deliberately break the system and require the test to notice. The industry equivalents are mutation-testing frameworks (Stryker, PIT) and chaos engineering’s steady-state hypothesis.
DORA / SREDirectly improves the validity of Change Failure Rate: a blind post-deploy gate under-reports failed changes, so the metric currently flatters us. The gate is a detective control whose own effectiveness is now measured.
Minimal decomposition4 sub-issues → 2 prompts.
Control / decisionFramework mappingHow it is tested / evidencedResidual risk
Falsifiability clause in the standardCPS 234 — control implementation commensurate with threat; ISO 27001 A.8.29 (security testing in development and acceptance)Published standard clause with a conformance checklist itemA clause is only as good as its gate; hence the two mechanisms below
Push-time shape gateNIST CSF 2.0 PR.PS (platform security, secure development); ISO 27001 A.8.28Gate ships its own regression test, proven red against the real unfixed tree per ADR-0141Shape matching catches known-blind forms, not novel ones — the red-proof covers the remainder
Periodic red-proof of every planCPS 234 — regular testing of control effectiveness (the closest direct mapping in the whole design: an acceptance gate that cannot fail is an untested control); NIST CSF 2.0 DE.CM / GV.OV; ISO 27001 Clause 9.1 (monitoring and evaluation of control effectiveness)Monthly sweep run record, per-service verdicts, dead-man proving the sweep itself still runsCoverage limited to services that boot standalone — unquantified, recorded below
Auto-rollback driven by a validated gateCPS 230 — operational risk controls and tolerance for disruption on critical operationsHandover proves the gate blocks a real blind plan and permits a conformant oneA rollback triggered on a false red is a new (small) disruption source
Lifecycle-based findings routingCPS 230 operational-risk record-keeping; NIST CSF 2.0 RS (response)Routing proven both ways at handover — snag for in-delivery, incident for acceptedDepends on service_lifecycle.yml being current
Real credentials in the harnessCPS 234 information-asset protection; ISO 27001 A.8.12 (data leakage prevention), A.5.15Env-file only, --network none, --rm, scrubbed output; reviewed at handoverCredentials transit a container image’s env for seconds; mitigated by having no network path out

Service record. This is estate tooling rather than a user-facing service, so it takes no NetBox asset record and no criticality tier of its own. It is a control over the test-plan gate, and its operating record is the standard plus this design. The service_lifecycle.yml overlay it introduces, however, becomes estate-wide service metadata and must carry a value for every service page.

Reliability contract. The SLI is proportion of test plans with a current, passing red-proof. The objective is 100% of red-provable plans proven within the last 35 days (the monthly cadence plus tolerance). The error budget is spent by any plan whose red-proof is stale or failing; the burn signal is the dead-man plus the sweep’s own finding count.

Runbook. docs/operations/ gains a page covering: how to run the harness against a single service by hand, how to read the three-way verdict, what to do when the harness cannot boot a service, how to route a finding by lifecycle, and how to clear a stale dead-man. It must be followable cold by a session with no context — proven by walking it at handover.

Test strategy. Per sub-issue: the shape gate ships a regression test proven red before it is trusted green (ADR-0141); the harness is proven by reproducing the tandoor result — the known blind plan must be reported BLIND, and a known-strong plan (qdrant’s canary round-trip) must be reported conformant. The mechanisms themselves are the deliverable, so “it ran without error” is explicitly not acceptance evidence for any of them.

Day-2 BAU and toil. One monthly automated sweep, declared in schedule_config.yml in a slot clear of the overnight backup and patching window (~22:00–07:00 AEST). No manual recurring work is introduced. The toil this removes is the per-service judgement about whether a test is any good, which currently has no mechanism at all.

Backup and restore. No state is created. The harness is stateless and its findings live in ADO and FreeScout, both already covered.

Standing owner. The monthly sweep is the standing mechanism; /health surfaces the dead-man so a stopped sweep is visible without anyone remembering to look. /vulnscan continues to consume test plans as its post-remediation close-gate and directly benefits from their strengthening.

Exit. If the red-proof proves not worth its cost, the sweep and its schedule entry are removed and the falsifiability clause degrades to the shape gate alone — a strictly better position than today.

#IssueBatch
#2413Falsifiability clause in the standard + push-time shape gate (with its own proven-red regression test)1
#2414Red-proof harness, CI on-change trigger, monthly sweep with dead-man, service_lifecycle.yml overlay1
#2415Fleet remediation — 8 blind plans, 6 metric conversions, 4 over_time windows2
#2416Handover / Production Readiness Review2

These were resolved by assumption during the grill and are named so that approval was not given blind:

  • How many services boot standalone is unmeasured. Tandoor did; netbox almost certainly will not. This directly bounds red-proof coverage and could shrink it materially. Measuring it is the first task of #2414.
  • The four retained metric plans rely on exporter-sever, a red-proof mechanism that has not been demonstrated to work.
  • service_lifecycle.yml must cover 66 service pages, not just the 34 carrying test plans; the effort to populate and keep it current is not sized.
  • Tightening *_over_time windows may introduce flapping in the four retained metric plans; no flap analysis has been done.

Filled by wrapup Check O at Epic close.