Plan: Contested-Environment Component Testing

On this page

Status

Program of record for epic &83 (anchor #1493). Plan v3 — a full rebuild after a maintainer stop-ship review found ten blocker-class defects in v2. Every correction is code-grounded (see Corrected core mechanisms (the ten blockers)). Honest size: ~97 weight across 25 units (v2’s 66/17 silently deferred docs-first sequencing, per-service fleet sweeps, two IN classes, and a closure phase).

Unit Description Status

C0

Plan docs MR alone — this page (nav Active, plan-lint clean), epic &83 body + child list, tracker repairs (#1466/#1493/#1404). Merges before any code.

Done (2026-08-18) — !1413 (b683b585); epic body Plan link + child scaffold; #1466/#1493/#1404 repaired

C1

ADR + runbook MR — two-layer doctrine, seam doctrine incl. the feature graph, crash-point contract replacement, the never-easier checklist, toxiproxy image/security posture. No hook/armer unit starts before C1 merges.

Done (2026-08-18) — !1414 (67be8d76); ADR-067 + arbitration runbook + never-easier checklist

C2

Issue-filing step — ~25 self-contained child issues (mechanism, files, oracles, acceptance criteria, weight, blocks-links), the macro-feedback escalation, tracker links.

Done (2026-08-18) — 22 issues #1498–#1519 (C3–C24) epic-linked + blocks-graph; #1493 closed; macro-feedback → cqs#27 (local #1520)

C3

Registry + census scanners (report-only) + fault recorder + cargo xtask fault-preflight.

Done (2026-08-18) — !1415 (1002705e); contested-surfaces.toml (60 surfaces), contested-census, fault-preflight, craig_test_lib::fault::record, census wired report-only into validate

C4

Toxiproxy sidecar — compose profile fault, digest pin, loopback-only control, pre-published 64-port range + cross-process lease allocator, hand-rolled reqwest client, ProxyGuard.

Done (2026-08-18) — !1416 (a7cdf5db); fault compose profile, craig_test_lib::fault::{port_lease,toxiproxy}, http_retry, bring_up --profile fault + wait_for_fault_health, fault-preflight docker-port/lease checks wired report-only into validate

C5

pg_terminate helper + DB-disconnect legs (backend-PID capture, admin-PID exclusion, advisory-lock barrier, same-role privilege, reconnect asserts).

Done (2026-08-18) — !1417 (1cc82234); craig_test_lib::fault::pg_terminate (backend-pid + admin-excluding terminate) + advisory-lock barrier + same-role/reconnect/rollback-clean leg

C6

PgFaultArmer — sequences-survive-rollback cadence, finish().await oracle, DSN hard guard, 4 statement classes, retire the `Faulty*Store`s, outbox/inbox crash-cell consumers.

Done (2026-08-18) — !1418 (5fa637ca); craig_test_lib::fault::pg_armer::PgFaultArmer (non-transactional SEQUENCE cadence surviving the induced rollback, finish().await PgFaultReport oracle, DSN denylist guard, 4 statement classes), restart.rs crash-cell wiring repointed to PgFaultArmer

C7

Feature graph + StageFaultInjector + OutboxPreSendInjector + release-artifact gate + RabbitDownInjector retirement.

Done (2026-08-18) — !1419 (05c37697); craig-mq fault-injection feature (arm_stage_fault in stage_event + PreSendInjector on Publisher), acyclic feature graph (self-dev-dep + craig-test-lib forwarding), BLOCKING release-artifact-gate folded into validate, PlacementPostUpdateOutboxPreSend+StageFault + RabbitDownInjector retirements

C8

CipherErrorInjector — zeroize-skip hook, operation-enum-only exposure (Encrypt|Decrypt|Hmac), full decrypt-path scope; PINS the existing redacted-500 contract.

Done (2026-08-18) — !1420 (14aa54f2); craig-crypto/fault-injection CipherErrorInjector (Arc-shared field on FieldEncryptor, Encrypt|Decrypt|Hmac, kcv excluded, zeroize-skip), the 5 decrypt surfaces proven at the shared craig_search::decrypt_row/encrypt_row seam, craig-common pins SearchError::Crypto→redacted-500

C9

ObjectStore injector + typed test-gated from_parts + request_timeout config (default 30s; error class unchanged).

Done (2026-08-18) — !1421 (535ba2ac); craig-store/test-util ObjectStoreErrorInjector (Arc-shared Store field), M10 typed from_parts(&StoreBackend) (no bool-spoof of the retention durability guard), request_timeout_secs (30s) threaded into the S3 ClientOptions, countdown dedup’d into the new craig-fault-core::OpCountdown<K> leaf (B8)

C10

craig-mq transport suite — per-variant graceful-close + TCP-reset legs on test-owned vhosts, confirm-timeout barrier, the AMQP slicer leg.

Done (2026-08-18) — !1422 (f3b4d0f8); craig-mq/tests/transport_faults.rs (10 legs: 4 subscribe variants × {graceful close, abrupt transport drop} + confirm-timeout + a 16 KiB byte-identical slicer oracle). L2 drop = Toxiproxy proxy DISABLE (ResetPeer no-ops on idle consumer sockets); non-vacuous NEW-connection reconnect barrier + redelivery-based exactly-once proof. craig_test_lib::fault::{mq_transport, toxiproxy::set_enabled}

C11

DB / object-store L2 legs — separated pool-acquire vs connection vs in-query legs; ambiguous-put readback/digest/orphan oracle.

Done (2026-08-19) — !1423 (e205f4a2); craig-db/tests/l2_db_faults.rs (3 distinct DB modes: pool-acquire→PoolTimedOut/5 s cliff, connect-refused→Io via direct connect, in-query drop→connection-lost family + recovery), craig-store/tests/l2_store_ambiguous_put.rs (downstream-reset ambiguous put → no orphan/no double-write), pg_transport::pg_url_via_proxy + garage_test_config + test-only Store::from_config_with_max_retries. Follow-up #1523 (object_store total-retry-latency bound)

C12

Burst driver + inbox permutation floods — staged attach/deliver/nack/re-attach; per-handler ordering contract; seeded permutations print the replay command.

Done (2026-08-19) — !1424 (9b3ac5dd); craig-mq/tests/burst_floods.rs (4 legs: duplicate storm + genuine broker nack/requeue → effects-exactly-once; revision-gated + commutative shuffles converge order-independently), craig_test_lib::fault::burst seeded driver, craig-mq/tests/common/ shared fixtures, 5 registry ordering declarations + the VALID_ORDERING taxonomy. Follow-up #1524 (fault-recorder CWD path)

C13

Inbox handler-contract legs, tranche 1 (cases, exchange, financial, reporting).

Done (2026-08-19) — delivered in sub-units (real-consumer-against-real-DB legs are heavier than C10-C12’s synthetic ones): U0 craig_test_lib::fault::mq_sever — the two-path sever/reconnect harness promoted out of transport_faults.rs so the SERVICE-crate legs can reuse it; then per-service U1 financial (template, revision-gated) / U2 exchange (twin, revision-gated) / U3 reporting (commutative) / U4 cases (real consumer via spawn_inbox_subscriber; a commutative rollup flood — exactly-once + order-independent — plus a two-path disconnect, over zero-entry clean-pass reports; the report_persons UNIQUE link convergence is the matching module’s idempotent-replay property, matching/mod.rs:470-582, covered there). All four tranche-1 mq-consumer surfaces → covered.

C14

Inbox handler-contract legs, tranche 2 (placement, security + DLQ, rules ×2, composition, authz invalidation) + 8 per-service outbox round-trips.

Done (2026-08-19) — MR-A (2870f61f) covered all SEVEN tranche-2 mq-consumers (placement revision-gated twin; security audit + DLQ via the real publish_dlxsubscribe_dlq path; rules domain-event + the novel exclusive cache-reconcile; composition + authz exclusive in-memory-cache legs). MR-B (U6) added the 8 per-service outbox stage→drain→consume round-trips over a shared assert_outbox_roundtrip harness (staged→drained→arrived→producer-exactly-once; scratch DB + scratch vhost). contested-census now reports 0 mq-consumers missing a two-path leg AND all 8 outbox-worker.* entries covered — zero uncovered IN-class MQ surfaces.

C15

S2S client matrix — the exact method-level existing-vs-missing matrix, then the fault legs; per-request deadlines decided per-client.

Done (2026-08-19) — #1510; the matrix is captured as machine truth in contested-surfaces.toml (10 s2s-client entries → covered with per-surface oracles + timeout/ambiguous legs; cli report-only for C18). 56 fault legs across 7 crates pinning ACTUAL behavior: craig-rules-client (was zero coverage — Fetch/Decode/404→None), craig-cases ScreeningPolicyClient (the pre-flag: fresh-cache short-circuit NOT stale-fallback → outage-after-TTL fails hard) + RulesEngineClient (transport-timeout→500-not-503 on all paths; the #1051 asymmetry — a rules-issued 503 passes through as our 503 on evaluate + safety, but every ruleset_metadata non-2xx folds to 500), craig-financial cases DOB/membership + placement eligibility (resolves the sg_create client-side S2S-down gap), craig-reporting cases-hop ISOLATED from token-mint, craig-exchange release_ssn retryable-vs-permanent custody classification, craig-intake CasesForwarderSink read/BOLA siblings, craig-web one Transport timeout leg discharging all ~33 bff_client! clients + the divergent delete/get_bytes/CompositionClient paths. Per-request deadlines: no new ones minted — financial’s 8s CASES_S2S_DEADLINE + placement’s 5s + exchange’s 5s custody budget are pinned as-is; the shared 30s/5s build_shared_client bound is the default (timeout legs use short-client-vs-delayed-mock, the production ceiling pinned separately). All legs in-process (no devstack). Fixed the stale clients/mod.rs doc (33 fields).

C16

Partner fault-triple — 9 mechanical caps-clones + 1 ssa-solq-shaped delta (audit excluded, types-only).

Done (2026-08-19) — #1511; 30 legs. 9 crates (cprs/doe-slds/empi/ies/ions/smile/stars/tcm/wic) each mirror the CapsAdapter reference triple through the real adapter + DirectHttpTransport against the mock-server (Latency→Timeout, Status(503)→BadStatus, MalformedBody→MalformedResponse). The ssa-solq delta drives the triple through the SSRF-vetting EgressControlledTransport (its distinguishing shape — the previously-uncovered egress send-timeout path) via the loopback-dev carve-out + raw-TCP stubs, SSN-canary asserted. craig-partner-audit OUT (types-only, recorded). 11 partner-crate registry entries; census 0 drift. Note: the caps PILOT triple is Latency/Status/Malformed — the mock-server cannot express a true TCP reset (ADR-067), so "reset" in the issue prose maps to the Status→BadStatus leg.

C17

Auth-plane fault legs — OIDC discovery / JWKS / introspection / JWS verify via wiremock.

Done (2026-08-20) — #1512; 13 legs, all in-crate wiremock (the decided-now doctrine), the first four auth-plane registry entries (hand-enumerated — craig-auth is outside the census AST scope). Every surface gains its first timeout leg (NONE existed anywhere on the auth plane) with an elapsed floor proving the tarpit engaged, plus a wrong-TYPE-JSON malformed leg (C16 doctrine); fail-closed on every fault — no class mints access. Actual-behavior pins: discovery does NOT stale-serve past its TTL while the JWKS key cache stale-serves indefinitely (the asymmetry now recorded in both files); the kid-miss on-demand refresh is a synchronous request-path IdP round-trip bounded by the fetch client timeout (floor AND ceiling asserted on BOTH the kid-miss and cold-cache self-heal paths, plus wiremock fetch counts so a suppressed refresh cannot pass vacuously); serve_on_outage covers ONLY the transport-error branch (a 503 is BadStatus and bypasses the stale window). Five pre-existing defects found and FILED, not fixed here: #1532 (JWKS refresh ignores HTTP status — json() on any status), #1533 (discovery outage collapses to EndpointNotConfigured — held by a PAIR: the new legitimate-cause leg + the pre-existing never-listening-port outage leg, two causes folding to one variant), #1534 (introspection zero-leeway vs the JWS path’s inherited jsonwebtoken 60 s default), #1535 (phantom cache-sweep-task doc), #1536 (the unbounded JWKS stale-serve — bound-or-record decision; the stale-serve leg is a marked CHARACTERIZATION pin, not a contract). The #1532/#1533 pins name their issue and change class when the fixes land. Timeout legs: short-client-vs-delayed-mock (C15 convention; production bounds = the compiled 10 s discovery/JWKS clients + build_shared_client 30 s/5 s, pinned at their sources).

C18

CLI contested legs — timeout, ambiguous response, no-unsafe-replay.

Done (2026-08-20) — #1513; 5 in-crate wiremock legs. Transport (client/transport.rs inline): tarpit → typed anyhow error carrying the reqwest timeout class with an elapsed floor (never a hang); a lost-response mutating POST surfaces the error with EXACTLY ONE received request (the transport never silently re-sends — the double-apply guard at this layer); ambiguous 503-with-HTML folds to a plain string, check_status refuses naming the status WITHOUT echoing the upstream body, one request only. Cmd path (cmd/adjustment.rs inline, driving the REAL clap-enum→body→POST path): --request-id reaches the wire VERBATIM across a scripted retry (the identity the server-side ADR-062 claim converges on), and a bare rerun mints a fresh v7 id (a NEW operation). Filed, not fixed: #1537 — the minted default id is never disclosed, so the lost-response recovery is unreachable on the default path (the legs prove exactly this). Timeout legs floor AND ceiling. Registry: s2s-client.craig-cli.mod flipped report-only→covered with the 5 test ids (the two cmd legs typed leg=replay). wiremock added to craig-cli dev-deps (the decided-now doctrine).

C19

Worker crash sweep — composition-invalidation, subsidy-generator, review-sweep; crash levers = M1 triggers + docker kill; graceful-drain a separate leg.

Done (2026-08-20) — #1514, the M1/in-process half; the docker-kill legs trail C20 by the dependency line (recorded per-entry in the registry). 7 legs. Generator: the crash-shaped partial state (committed child + mid-tx-dead child via the child-gated trigger, the J-accepted constructed-crash precedent) replays with ZERO money duplication proven at all three layers — report confirms (already_existed), the partial-unique arbiter holds one live row, exactly ONE payment_created per row for the target month across runs (windowed per #1108 + an orphan detector), and the dead tx leaves no row and no event (abort on the insert, before the stage is reachable). Review-sweep: ORACLE CORRECTED from 'resumes from durable lease/cursor' — the lease is a session advisory lock (self-freeing, deliberately non-durable) and NO persisted cursor exists; the durable artifacts are run rows + pinned sets + per-agreement txs, and resume = convergent re-scan (pinned across three runs in ledger AND event outbox; an enforcement tx aborted at its LAST statement — the event INSERT, after ledger/projection/queue had executed — rolls back whole). The stranded-executing crash residue is pinned as ACTUAL naming #1538 (nothing reaps it; filed, not fixed). Composition-invalidation: no durable cursor BY DESIGN — the event_outbox.published_at M1 cell (real PgFaultArmer on a scratch template DB + real OutboxWorker + real consumer: publish-ok/stamp-dies → aged-claim re-drain → republish re-clears idempotently and scoped) + the crash-window characterization (invalidations during downtime are LOST, TTL backstop — doc-folklore made load-bearing, with an ordering proof). Drain legs: cancel ends both financial scheduler loops promptly under a peer-held lease (the loop-level select! nothing else pinned; spawn fns narrowed to the fields they read — behavior-identical — for constructibility; the drain tests join the lease nextest group, a semantically-required serialization). Composition’s drain = subscribe_exclusive cancel, already pinned in craig-mq (recorded, not duplicated).

C20

MultiReplicaCluster redesign — dedicated docker-compose.cluster.yml, no fixed host-ports, scratch vhost/DB, stage-owned teardown; out-of-band (weekly CI + on-demand).

Done (2026-08-20) — #1515; the rig is docker-compose.cluster.yml (self-contained: craig-financial ×2 + own postgres/rabbitmq/keycloak + the ADR-063 migrate gate; ZERO fixed host-port publishes — every publish is the bare-container-port ephemeral form; resource limits on every service; no named volumes) under craig-cluster-{run}. DEVIATION recorded (living spec): 'scratch vhost/DB' is satisfied by the rig owning its ENTIRE broker/DB — a scratch vhost inside a shared broker was the old design’s need; a dedicated stack makes it moot. Lifecycle is stage-owned: cargo xtask cluster-tests = stale-prefix sweep → build (ONE app image — single cargo build, inside the #1347 rule’s rationale) → up -d --wait (healthchecks ARE readiness) → the two kill legs (--test-threads 1 — ONE shared rig, semantic ordering; and a nextest slow-timeout override to 120s×5 for the two legs ONLY — container orchestration inside a leg cannot live under the 120s unit kill; both out-of-band-only, called out per the never-easier checklist) → test-results/cluster/ artifacts on both outcomes → down -v, ctrl-c-safe (xtask’s first signal handler). Proof runs: both legs green on the live rig (docker-kill ~59s incl. restore; mgmt-close ~5s) with replica-anchored oracles (connection-name client IP ↔ container IP — names alone could be satisfied by two consumers on one replica). The proof runs earned their keep twice: run 1 proved the interference the --test-threads 1 serialization exists for, and run 3 caught a REAL tag-drift bug (a test-side restore under the default image tag made compose recreate the healthy survivor — the rig tag is now single-sourced across every compose evaluation). Legs (per-connection-NAME oracles — a bare count is vacuous with a surviving second replica): mgmt-close = closed name vanishes + a NEW name appears + the kept name persists + no container restarts; docker-kill = the broker sheds the dead replica via TCP death, Postgres reaps every backend by client_addr (the session-advisory-lease release mechanism observed for real), restore() re-attaches with the survivor untouched. MultiReplicaCluster redesigned: cluster-file-only, up --wait, async process invocations, Drop best-effort teardown for the self-managing smoke path, attach() for stage-owned runs, sweep_stale_projects. CI: the repo’s first DISCRIMINATED schedule (CLUSTER_TESTS=true guard — undiscriminated schedule rules co-fire on every schedule) + the first MR-label gate (cluster-tests) + manual; allow_failure per the standing devstack-job policy. ADR-067 § Amendment — #1515 records the L3 lever class. The three C19 worker kill-leg oracles land on the rig as #1539 (post-C24 backlog; the C20 ACs cover cluster recovery).

C21

Pool-contention harness + report-only characterization (M=4 / acquire 5s / K=8 / Barrier); promotion threshold = #1404’s design merged.

Done (2026-08-20) — #1516; craig_test_lib::fault::pool_contention runs the pinned matrix over the REAL DbPool (the production 5 s serving bound — the harness deliberately exposes no acquire knob), warmed so the M-winner/K−M-cliff split is STRUCTURAL (load moves latencies, not counts; honest bound recorded: poll starvation past hold−bound (2 s) would flip the split — and red the test loudly; measured: 4 wins at ~0 ms, 4 PoolTimedOut at ~5001 ms). The bound is derived from the newly-pub craig_db::SERVING_ACQUIRE_TIMEOUT (compile-time pinned hold>bound; still not a knob); the pool-class executed-fault record lands workspace-anchored. Characterization → test-results/pool-contention.json, write anchored at the workspace root (the #1524 recorder CWD class does not recur). The consuming test asserts harness integrity only (split + accounting + no third error class) — REPORT-ONLY per the #1404 boundary; the first pool-class registry entry records the promotion threshold.

C22

Degraded-UX e2e — cargo xtask e2e --degraded, docker-compose.fault.yml overlay, the 8-row flow matrix; its own serial stage.

Done (2026-08-20) — #1517. Real-stack arming (page.route() ratified out): the overlay repoints craig-web’s EIGHT backend URLs at STANDING port-mirrored toxiproxy pass-throughs (toxiproxy:8002craig-cases:8002), so a spec takes a backend down (proxy disable → connection-refused in ms) or makes it flap (timeout toxic) WITHOUT touching the backend container — its healthcheck, depends_on graph, MQ consumers and seed stay untouched. Proxies are named craig-degraded- so the Rust legs' lease-less craig-fault- sweep can never delete them mid-stage, and the listen ports sit OUTSIDE the 24600-24663 lease pool (unpublished in-network ports need no lease). Stage safety, all four plan requirements proven live: the devstack lifecycle lock is held for the WHOLE stage (the ordinary e2e path holds it only transiently inside its nested dev dispatches); a DURABLE dirty marker (.devstack/degraded-overlay) is armed BEFORE the overlay recreate; ensure_ready auto-reconciles a stranded overlay (dev start/validate/gated mutantsdev reseed/dev clean tear down to the base shape instead; proven live: marker armed → dev start restored + cleared), gated on a RUNNING craig-web so a marker outliving its stack never force-recreates a one-container devstack; restore is a base --force-recreate + post-restore health check, running on success, failure, and SIGINT alike, and a failed restore deliberately leaves the marker armed for the next converge. The 8 rows (13 tests incl. auth setup, 7.1 s) pin ACTUAL UX including its defects. FIVE defects found and filed, not fixed: #1540 (detail pages 404 on outage — indistinguishable from record-missing), #1541 (forms lose ALL typed data), #1542 (attachment-download failure is a silent redirect), #1543 (login silently redirects on IdP outage), #1544 (blank optional Duration returns a RAW axum deserialization error — surfaced by a row’s first draft). Also recorded: toxiproxy’s control API refuses BROWSER user agents (403 "User agent not allowed"), so the spec helper must send a non-browser UA — found live, every row 403’d until it existed.

C23

Registry promotion to blocking — nextest-list test-existence validation, bidirectional drift, --bless, exact-marker classes → blocking.

Done (2026-08-20) — #1518. FOUR classes promoted to BLOCKING (mq-consumer, outbox-worker, worker, partner-crate — the exact-marker classes whose anchors the AST census discovers, so drift/staleness are machine-decidable with no judgment). partner-crate EARNED that footing here: the census was widened to crates/craig-partner-/src/*, discovering a partner adapter’s map_transport_error by DEFINITION — and it immediately found a real gap, craig-exchange’s shared StandardAdapter (registered report-only; legs tracked as #1545). Also: the SEVEN hand-enumerated classes (s2s-client, browser-flow, auth-plane, crypto, object-store, pool, cli) stay report-only with their promotion criterion recorded IN CODE beside the table: a census that enumerates them structurally (expanding bff_client! / walking the route table), so an unregistered surface becomes a fact rather than an opinion. A unit test pins that the two tables PARTITION the class vocabulary — a new class cannot land in neither and go un-reasoned-about. For a promoted class FIVE finding kinds now fail the build: coverage drift, a stale anchor, an mq-consumer two-path gap, a covered entry with no tests, and a tests id no test binary declares. TEETH PROVEN BY INJECTION, not assumed: a bogus test id, a broken anchor, and a removed tcp-reset leg each produced exit 1 with a precise finding. Test-existence runs cargo nextest list --workspace --locked --run-ignored all --message-format json (plan M8’s exact-name form — the first draft’s substring matcher would have let a rename to a SUPERSTRING pass, which the tightened matcher now catches) — ALWAYS on in validate, opt-in (--verify-tests) for the ad-hoc census where it costs ~2 min cold. Cost recorded honestly: in a full validate the compile is shared with the battery, but under --skip-devstack (battery = --lib --bins) it forces a --workspace test-target compile that mode otherwise avoids, so the step announces itself before going quiet. When verification CANNOT run, validate FAILS rather than reporting a clean zero. --bless writes its stub status through one constant with an in-code assertion that it can never mint covered (rule 6).

C24

Program-verification gate — the blocking command/job matrix (no-skip validate with fault REQUIRED, every-armed-fault-fired, panic/process-death self-test, production-DSN refusal, loopback runtime check, artifact retention). Merging C24 is the #1466 (B5) start trigger.

Done (2026-08-21) — #1519. cargo xtask program-gate + the validate PAIR: program_gate::reset purges every executed-fault record and stamps test-results/fault/.gate-stamp BEFORE nextest, program_gate::verify runs after it. That pairing is the unit’s own correction — records accumulate across runs, so aggregating post-battery alone would have made "the battery executed faults" a claim about the DISK, passing green on yesterday’s evidence. The J-review then found the pairing’s own hole and it was closed before commit: the stamp is SINGLE-USE (a SUCCESSFUL paired verification consumes it, a failed one leaves the evidence intact for triage, and the standalone command never consumes it) and its absence is FATAL under validate’s policy (a vanished stamp meant the pairing broke or the run was already certified — the first draft downgraded it to a warning and passed); a record predating the stamp fails the gate (unit-tested through check_freshness, not just its helper, by aging a survivor’s mtime); the aggregation COUNTS unreadable record files and any such count refuses certification, so a truncated record from a killed test is missing evidence rather than a silent zero; and fault dirs are DISCOVERED by walking the checkout (symlinks unfollowed) instead of a hand-listed crates/services/tools, which had made a new workspace member’s records invisible to purge, freshness and aggregation alike. fault-preflight flipped report-only → --required in validate’s devstack phase (the --skip-devstack CI path never reaches it — the one sanctioned fault-optional mode, recorded). The panic/process-death self-test (crates/craig-test-lib/tests/program_gate_cleanup.rs) pins the ACTUAL two-stage contract, not a wished-for one: stage 1 asserts the residue SURVIVES the panic (ProxyGuard/PortLease are detects-only — a future "helpful" async-cleanup Drop would be an ADR-067 §M2 violation and this assertion is what catches it), stage 2 proves sweep_stale_proxies + the lease TTL reap it (the test ages its own lease row — the only simulation; the sweep, the live-lease filter and the delete are real). Verified live, panic confirmed firing. The production-DSN refusal + loopback runtime checks were already tests/preflight steps, so the gate PINS them by name against the nextest inventory (phase 6) rather than re-implementing them — a rename or delete fails the gate instead of quietly retiring the guarantee. HONEST per-class floor: M9’s ">0 executed faults for EVERY IN class" is unenforceable today because only mq-consumer + pool call the recorder (measured: 89 + 6 = 95 fired, 0 unfired), so the gate enforces those two and NAMES the other nine as owed on every run (#1546 filed with the adoption criteria) — enforcing wholesale would fail for a bookkeeping gap, not a coverage gap. The retained fault-report.json carries the owed classes in classes_with_zero_fired, so the residue survives in the artifact, not just stdout (the #1524 CWD scatter still stands; #1524 owns the fix). As-built: ADR-067 §D9 note + the runbook’s section.

Epic: &83
Anchor: #1493 (plan-authoring)
Branches: one per unit (docs/… for C0–C1, feature/…/test/…/chore/… per unit thereafter)
Scale: ~97 weight / 25 units (canonical 1/2/3/5/8)
Critical path: C0 → C1 → C6 → C12 → C13/C14 → C23 → C24

Context

Battery failures have repeatedly cost afternoons of forensics because nothing separates "the component mishandles adversity" from "the test/fixture/envelope is bad" (2026-08-17: three burned cycles on docker-devtools daemon contention). Maintainer direction: adversity becomes a first-class, DETERMINISTIC test input — retries, races, broker disconnects, bursty/duplicate/ out-of-order delivery, pool exhaustion — forced by the test, never a quiet-box assumption or perf-at-scale. Two more maintainer requirements: cover this test shape across the WHOLE codebase, and give new code an easy integration mechanism so future surfaces land with contested legs by construction.

Second purpose — the arbitration instrument. A green per-component contested contract + a flaky battery ⇒ the defect is in the test/fixture/envelope (or a capacity attribution); a red contract ⇒ a real component defect WITH a reproducible case. This gives the no-environmental-blame rule an objective backstop and should directly reduce battery failures.

Standing constraint (maintainer, 2026-08-17): tests never get easier. No added serialization beyond semantically-required ordering; no envelope widening; no assertion loosening; tests stay exposed to ambient load. "The application needs to be able to handle a contested environment. Life is never 0% system usage." Every J-review in this program runs the The never-easier checklist (created in C1; applied in every J-review) checklist (zero nextest.toml serialization/envelope hunks unless semantically required and called out).

This is plan v3 — a full rebuild after a maintainer stop-ship review found ten blocker-class defects in v2 (transactionally-impossible trigger audit, async-in-Drop, wrong injection boundary, an unviable feature graph, schedule-coupling from killing live consumers, unscheduled "whole-codebase" coverage, a scanner blind to the 33-field BFF macro family, false-pass/ false-fail ratchets, silent-green skips, a production durability bypass). Every correction below is code-grounded.

Ratified forks (maintainer, 2026-08-18)

  • F1 posture — in-battery REQUIRED (no silent skips; see Fault stage (required mode, no silent skips)). L10 (cluster) out-of-band; the e2e leg (C22) is its own serial stage.

  • F2 fault hooks — feature-gated via an exact acyclic graph + a release-artifact gate (a dev-dependency cannot enable a parent’s feature; see [feature-graph]).

  • F7 — keep the cipher injector (now C8).

  • Sequencing — epic &83 to COMPLETION (C24 merged) before #1466 (B5) starts.

  • Engineering-decided — the ratchet is a maintained typed REGISTRY (not grep markers); enforcement starts report-only, promotes to blocking per class at C23; the #1404 boundary = this program ships the pool-contention harness + report-only characterization, #1404 keeps the production typed-degradation design.

Corrected core mechanisms (the ten blockers)

M1 — PgFaultArmer cadence/audit survive rollback. A trigger that increments a counter and inserts an audit row then RAISE`s loses both to the rollback (the `inbox_tx.rs:439 precedent leaves no trace). Fix: cadence = one non-transactional SEQUENCE per armed fault (nextval() persists across rollback) — IF nextval('craig_fault_seq_{id}') = N THEN RAISE; audit = the Rust side reads last_value/is_called between phases. PgFaultArmer does NOT implement FaultInjector (it can’t honor recorded_attempts()); it returns its own typed PgFaultReport { invocations, injected, passed }. Statement classes decided now: event_outbox.published_at, event_inbox.processed_at, exchange_send_jobs.status, upload_attempts.status. Cleanup = explicit disarm().await + scratch-DB disposal backstop (DDL dies with the per-test database); never Drop-dependent.

M2 — no async assert in Drop. ScenarioGuard holds a sync FnOnce and suppresses under panic. Armers with external (DB) oracles expose async fn finish(self) → Report / async fn assert_consumed(self), called in the test body; Drop only panics on a forgotten finished flag (never reads the DB). The sync ScenarioGuard stays for in-process injectors (cipher/object-store/publisher) whose counters are process-local.

M3 — publish-fault boundary split (PlacementPostUpdate was unimplementable). Placement stages events IN the SQL tx (events.rs:3); the handler’s Publisher extractor is unused (placements.rs:669); only the outbox worker later publishes. So: (a) a StageFaultInjector hook inside craig_mq::stage_event (outbox.rs:86) — failure rolls back domain UPDATE + outbox row together (atomicity leg); (b) OutboxPreSendInjector — the F2 hook on Publisher before basic_publish (publisher.rs:141), armed during drain — committed row, pending outbox, later exactly-once publish (the semantics PlacementPostUpdate wanted, at the real boundary). CrashPoint::PlacementPostUpdate retired → OutboxPreSend + StageFault; the third cell (publish ok, stamp fails) is M1’s event_outbox.published_at trigger.

M4 — feature graph (acyclic opt-in + release gate). Host features craig-mq/fault-injection, craig-crypto/fault-injection, craig-store/test-util gate the hook FIELDS (a field must live in the host struct). Rules: craig-test-lib’s normal deps NEVER enable them; it has its own default-off forwarding feature; xtask depends on test-lib WITHOUT the feature (lint-guarded); host integration tests self-dev-dep with the feature (resolver-v2 test-only activation); cross-crate consumers dev-dep test-lib with the feature. Release-artifact gate (new blocking lint): cargo tree -e normal,build over the Dockerfile -p list + xtask must show the features absent. Honest claim: "absent from release artifacts and every normal-dep graph, machine-enforced" — not "impossible" (they compile under --all-features).

M5 — U2 never kills live devstack consumers. consumer_connection returns the FIRST attachment (races other tests under 8-way nextest); depth is not an oracle. Fix: each disconnect leg creates a unique vhost (craig-test-{run}-{test}), starts ITS OWN consumer there, identifies the connection as "the only one in my vhost", and asserts on event_inbox.processed_at + handler effect rows in its scratch DB. Barriers staged: attach → probe → domain oracle → close by exact name → reconnect barrier → probe-2 → exactly-once. Per-service handler-contract legs move to service crates (a generic craig-mq test can’t import private handlers).

M6 — two-path whole-codebase coverage is scheduled (see Coverage schedule (two-path, every surface, named location)): both legs (graceful AMQP close + TCP reset) per subscribe variant at the library layer (C10), plus per-callsite handler legs (C13/C14) and all 8 outbox round-trips (C14). The registry’s two-path rule makes any residue a blocking failure at C23.

M7 — S2S census = maintained typed registry. syn can’t expand bff_client! (33 fields at clients/mod.rs:201); sinks like CasesForwarderSink are invisible. The registry (contested-surfaces.toml) is truth; scanners catch omissions by reading syntactically-exact things: bff_client! macro-call args, build_shared_client consumers, and explicit // contested-surface: <id> markers for bespoke types.

M8 — ratchet = registry + nextest-validated test IDs + registration inventory. .subscribe( is type-unresolved; heartbeat literals miss dynamic names and collapse shared helpers. Fix: surface→test-ID mapping in the registry, validated against cargo nextest list --message-format json (a named test must compile/exist); MQ census = AST walk of direct subscribe-family calls under services/ + craig-authz, excluding #[cfg(test)]/tests/ callsites; worker census = supervisor watch*( REGISTRATIONS, not heartbeat text. Report-only at C3; exact-marker classes promote to blocking at C23.

M9 — required-mode fault stage, no silent skips. The fault_layer_available() boolean probe is DELETED. Tests are #[ignore = "requires devstack+fault"]; cargo xtask fault-preflight (a validate step before nextest) hard-fails when the fault layer is absent; ensure_ready gains fault-profile health (devstack.rs:129 only checks craig-web today); executed-fault accounting (test-results/fault/*.jsonfault-report.json) asserts every armed fault fired and every IN class has >0 executed faults — a green battery with zero executed faults is a FAILURE.

M10 — no durability bypass. Store::from_parts is #[cfg(any(test, feature = "test-support"))] and takes the typed StoreBackend enum (never a backend_is_local: bool); is_local_backend() keeps deriving from the typed enum that retention’s boot guard trusts (boot.rs:37). from_config untouched.

Scope

In scope:

  • Deterministic, forced adversity for every IN-class production surface (see Surface taxonomy — full-codebase coverage map): transport disconnects (graceful + TCP-reset), confirm timeouts, burst/duplicate/out-of-order floods, commit-boundary crash windows, cipher/object-store faults, S2S/partner/auth-plane/CLI fault triples, worker crash-and-resume, DB-pool contention (harness + report-only), degraded-UX e2e.

  • A maintained typed registry (contested-surfaces.toml) + census scanners so new surfaces land covered by construction; report-only first, promoted to blocking per class at C23.

  • A required-mode fault stage with executed-fault accounting (a zero-fault green battery fails).

Out of scope (recorded, with reasons in Surface taxonomy — full-codebase coverage map):

  • SDKs (already ratcheted: retry-same-id pins + shared test-vectors).

  • Tool-time surfaces (craig-cli ops / craig-seed / xtask migrate,import,archive-fetch / mock-server — no production request path).

  • The composition engine (pure/deterministic, property-covered; its runtime rides classes 1/3/4).

  • The production typed-degradation design itself (#1404) — this program ships only C21’s harness
    report-only characterization; the C21 enforcement promotion threshold = #1404’s design merged.

  • Cluster L10 legs run out-of-band (weekly CI + on-demand), not in the per-push battery.

Surface taxonomy — full-codebase coverage map

# Class Enumerated by IN/OUT Units

1

S2S HTTP clients

registry: 33 bff_client! + bespoke BFF + 9 domain clients + craig-rules-client + intake sinks

IN

C15

2

External partners

11 craig-partner-* over transport (audit is types-only → OUT) + SHINES + SSA/SOLQ

IN

C16

3

MQ surfaces

AST: subscribe/idempotent/exclusive/dlq ×N + spawn_outbox_worker ×8 (services/ + craig-authz)

IN

C10, C12, C13, C14

4

Background workers

supervisor watch*() registrations

IN

C19 (+ drain legs)

5

DB pool

bootstrap pool configs

crate-battery authoritative; harness IN (C21), per-service ratchet OUT (recorded)

C5, C11, C21

6

Crypto / keyring

FieldEncryptor consumers; keyring persist-fault (already tested)

IN

C8

7

Browser / BFF

e2e flow matrix (C22)

IN

C22

8

Auth-plane

OIDC discovery / JWKS / introspection / JWS verify (already workers)

IN (new)

C17

9

CLI

authenticated mutating client (client/mod.rs:14)

IN (moved from OUT)

C18

10

SDKs

retry-same-id pins + shared test-vectors

already ratcheted (recorded)

11

Tool-time

craig-cli ops / craig-seed / xtask migrate,import,archive-fetch / mock-server

OUT (no production request path — recorded)

12

Composition engine

pure / deterministic

OUT (property-covered); runtime rides classes 1/3/4

Design — the units by phase

Phase A — governance (docs land FIRST)

C0 (w2, deps none): plan docs MR alone. This page (nav Active, plan-lint clean), the epic &83 body + child list, tracker repairs (#1466: drop stale blocked-by-#1479, encode the "starts when C24 merges" gate; #1493 supersede note; #1404 boundary). Merges before any code.

C1 (w3, deps C0): ADR + runbook MR. Two-layer doctrine + different-recovery-paths rule; the seam doctrine incl. the M4 feature graph; the crash-point contract replacement (M3); RabbitDownInjector retirement; PgFaultArmer trait divergence (M1); U6 redacted-500 affirmation; toxiproxy image repo/tag/DIGEST + healthcheck + SBOM/license path + digest-refresh owner + control-plane security posture (loopback-only, isolated network, non-root, limits); OUT-class justifications; the The never-easier checklist (created in C1; applied in every J-review) checklist (CREATED here — it does not exist yet); the arbitration-ladder runbook
no-environmental-blame wiring. No hook/armer unit starts before C1 merges.

C2 (w2, deps C0, C1): issue-filing step. ~25 self-contained child issues (mechanism, files, oracles, acceptance criteria, weight, blocks-links), the macro-feedback escalation (tests/fault_injection/ vs tests/fault/ template drift), tracker links.

Phase B — foundations

C3 (w3, deps C1): registry + census scanners (report-only) + fault recorder + preflight. contested-surfaces.toml (Registry design (contested-surfaces.toml, repo root, CODEOWNERS-guarded)), the AST/macro/marker census cross-check, the executed-fault JSON recorder, cargo xtask fault-preflight.

C4 (w5, deps C1): toxiproxy sidecar. Compose profile fault, digest pin, loopback-only control binding, PRE-PUBLISHED port range (64 ports) + cross-process lease allocator (fault_port_leases in the test-plane DB, TTL + dead-run sweep), run+test-scoped proxy names, hand-rolled reqwest client (rabbitmq_mgmt.rs shape), ProxyGuard explicit destroy().await + stale-only sweep, ensure_ready fault-health.

C5 (w3, deps C1): pg_terminate helper + DB-disconnect legs. Target pg_backend_pid() capture + admin-PID exclusion + advisory-lock in-phase barrier + same-role privilege spec
reconnect/recovery asserts.

C6 (w5, deps C1): PgFaultArmer (M1 + M2: sequences, finish().await, DSN hard guard, 4 statement classes, retire the Faulty*Store`s, outbox/inbox crash-cell consumers, `restart.rs wiring update).

C7 (w3, deps C1): feature graph + StageFaultInjector + OutboxPreSendInjector + release-artifact gate + RabbitDown retirement (M3 + M4).

C8 (w3, deps C1, C7): CipherErrorInjector. zeroize-skip hook, operation-enum-only exposure (Encrypt\|Decrypt\|Hmac; kcv excluded), full decrypt-path scope (persons_search, persons_federal_export, reports/summary, ssn_promotion, referrals); PINS the existing redacted-500 contract (non-500 degradation is an API change, OUT).

C9 (w3, deps C1, C7): ObjectStore injector + typed test-gated from_parts + request_timeout config (M10; default 30s, error class stays StoreError::ObjectStore).

Phase C — transport + library legs

C10 (w5, deps C4): craig-mq transport suite. Per-variant graceful-close + TCP-reset legs on test-owned vhosts (M5/M6); confirm-timeout with a channel-established-first phase barrier (arm the downstream timeout toxic AFTER a clean confirm, else you get PublishError::Amqp not ConfirmTimeout); the AMQP slicer leg (moved here from v2’s U8).

C11 (w3, deps C4, C9): DB / object-store L2 legs. Separated pool-acquire vs connection vs in-query legs; ambiguous-put readback/digest/orphan oracle; the 5s cliff restated.

C12 (w5, deps C6): burst driver + inbox permutation floods. Staged attach/deliver/nack/re-attach (no live-consumer race, per M5); per-handler ordering contract (commutative\|revision-gated\|buffered\|strictly-ordered) declared in the registry; seeded permutations print CRAIG_BURST_SEED + the replay command.

Phase D — fleet sweeps

C13 (w5, deps C6, C12): inbox handler-contract legs, tranche 1 (cases, exchange, financial, reporting).

C14 (w5, deps C6, C12): tranche 2 (placement, security + DLQ, rules ×2, composition, authz invalidation) + 8 per-service outbox staged-event→drain→consume round-trips.

C15 (w5, deps C3): S2S client matrix. Deliverable #1 = the exact method-level existing-vs-missing matrix (financial already has the hanging-cases test main.rs:1591; reporting’s test fails at token-mint not the cases hop — isolate them; ScreeningPolicyClient has fresh-cache short-circuit, NOT stale-fallback — pin actual behavior; financial API-path typed-503 vs MQ-path retry/DLQ; build_shared_client already gives 30s/5s — no "add if absent"). Per-request deadlines decided per-client in the matrix (financial CASES_S2S_DEADLINE precedent only where a latency contract requires).

C16 (w3, deps C3): partner fault-triple. 9 mechanical caps-clones + 1 ssa-solq-shaped delta (audit excluded, types-only).

C17 (w3, deps C3): auth-plane fault legs. OIDC discovery / JWKS / introspection / JWS via wiremock.

C18 (w2, deps C3): CLI contested legs. Timeout, ambiguous response, no-unsafe-replay.

C19 (w5, deps C6; kill legs trail C20): worker crash sweep. Three workers (composition-invalidation, subsidy-generator, review-sweep). Crash levers = M1 triggers
docker kill (CancellationToken is graceful drain, a SEPARATE leg). Oracles: cache re-clears idempotently; zero money duplication on generator replay; sweep resumes from durable lease/cursor.

Phase E — heavy rigs

C20 (w5, deps C12, OUT-OF-BAND): MultiReplicaCluster redesign. Dedicated docker-compose.cluster.yml (ONE service + minimal own infra, NO fixed host-port publishes, project craig-cluster-{run}, scratch vhost/DB, healthcheck readiness, resource limits); stage-owned teardown + signal handler + stale-project sweep; artifacts to test-results/cluster/; kill = docker kill (OS crash) vs mgmt close (reconnect), both legs; cargo xtask cluster-tests + weekly CI job + on-demand label.

C21 (w3, deps C1): pool-contention harness + report-only characterization. Pinned M=4 / acquire 5s / K=8 / Barrier start; report test-results/pool-contention.json; promotion threshold = #1404’s design merged.

C22 (w8, deps C4, C1): degraded-UX e2e. cargo xtask e2e --degraded with the SAME devstack lifecycle lock + durable dirty marker + ensure_ready auto-reconciliation + signal-aware cleanup + base --force-recreate restore + post-restore health checks; the docker-compose.fault.yml overlay; the 8-row flow matrix (page/action × failed dependency × down\|flapping × expected UI × retry affordance × no-data-loss oracle).

Phase F — ratchet + closure

C23 (w5, deps C10, C12–C19): registry promotion to blocking. nextest-list test-existence validation, bidirectional drift, --bless (stubs as report-only, never covered), exact-marker classes → blocking, S2S/browser stay report-only with recorded promotion criteria.

C24 (w3, deps C20, C22, C23): program-verification gate. The blocking command/job matrix: final no-skip validate with the fault profile REQUIRED, every-armed-fault-fired assertion, panic/process-death cleanup self-test, production-DSN-refusal test, loopback-only runtime verification, artifact retention. Merging C24 is the #1466 (B5) start trigger.

Dependency graph

Blocks-links (the C2 issue-filing step encodes these):

C0 → C1 → C2
C1 → {C3, C4, C5, C6, C7, C21}
C7 → {C8, C9}
C4 → {C10, C11, C22}
C9 → C11
C6 → C12
C12 → {C13, C14, C20}
C3 → {C15, C16, C17, C18}
C6 → C19 ; C20 → C19 (kill legs)
{C10, C13, C14, C15, C16, C17, C18, C19} → C23
{C20, C22, C23} → C24

Critical path: C0 → C1 → C6 → C12 → C13/C14 → C23 → C24.

Registry design (contested-surfaces.toml, repo root, CODEOWNERS-guarded)

Per-surface entry: stable dotted id; class (mq-consumer\|outbox-worker\|worker\|s2s-client\|partner-crate\|crypto\|object-store\|pool\| browser-flow\|cli\|auth-plane); crate; anchor {file, call, nth}; ordering (inbox handlers only); levers; tests [{id, leg, via}]; oracle; status (covered\|report-only\|skip — skip requires reason/owner/review_by).

Validation:

  1. anchors resolve via AST or fail (stale);

  2. census ⊆ registry or fail (new uncovered surface);

  3. every test id ∈ nextest list (+ Playwright manifest);

  4. every mq-consumer references BOTH a graceful-close and a tcp-reset leg;

  5. skip needs reason/owner/unexpired review_by;

  6. --bless writes report-only stubs, never covered.

Coverage schedule (two-path, every surface, named location)

Library layer C10 discharges both legs per variant (subscribe/idempotent/exclusive/dlq × {graceful, reset}). Per callsite: cases/exchange/financial/reporting inbox → C13 handler legs; placement/security(+dlq)/rules(×2)/composition/authz → C14; 8 × spawn_outbox_worker → C10 drain transport (pre-send hook + confirm-timeout) + C14 per-service round-trip. The registry’s rule 4 is the machine check that nothing IN-class is a baselined exception at C24.

Fault stage (required mode, no silent skips)

required is the default in validate + the contested CI job (an absent layer ⇒ hard fail before nextest); optional only for explicitly-invoked raw dev runs + the no-devstack CI job. fault-preflight: toxiproxy /version healthy, loopback binding verified via docker port, fault services up, lease table reachable. Executed-fault accounting asserts every armed fault fired and every IN class has >0 executed faults.

Budget methodology

Measured baseline first: pin the 3-run median of cargo xtask validate before C4 merges (the v2 "+3–6 min" claim is withdrawn as unsupported). Ceilings: contested craig-mq ≤4 min at 8-way; per-leg ≤90s (setup ceiling, never assertion widening); fault-preflight ≤20s; e2e --degraded its own serial stage ≤10 min; cluster-tests out-of-band (no battery budget). A ceiling breach is a blocking finding — split or fix the fixture; widening needs a maintainer-approved amendment.

The never-easier checklist (created in C1; applied in every J-review)

  • Zero .config/nextest.toml serialization/envelope hunks — unless a test is semantically order-dependent AND the ordering is called out in the review.

  • No timeout/envelope widening to make a leg pass; a setup ceiling (≤90s) is a fixture ceiling, never an assertion-widening budget.

  • No assertion loosening, no [ignore] to dodge a real failure (the M9 [ignore] is fault-layer gating, enforced by fault-preflight, not a skip).

  • Tests stay exposed to ambient load — a leg must not quiesce the box to pass.

  • A ceiling breach or an added serialization is a blocking review finding, split or fix the fixture; a genuine exception needs a maintainer-approved amendment recorded here.

Decided-now (no "at implementation" residue)

Trigger statement classes (M1); wiremock for in-crate S2S/auth/CLI, mock-server for e2e + the partner triple; object-store both legs + request_timeout(30s); cipher ops Encrypt\|Decrypt\|Hmac (kcv out); the C21 matrix (M=4 / K=8 / 5s / Barrier + JSON report + #1404 promotion threshold); C22’s 8-row flow matrix; C19 crash points + money-duplication/lease-resume/idempotent-cache oracles; per-request deadlines per-client in the C15 matrix; reporting token-mint isolation; U6 redacted-500 affirmed; toxiproxy digest/SBOM/refresh-owner as C1 acceptance criteria.

Corrected narrative (v2 claims that were false)

"Three of four injectors need zero production code" → only PgFaultArmer is zero-production (object-store/publisher/cipher add feature-gated code). "All four finished" → three built, one (RabbitDown) retired, PublishInTx split+renamed. The keyring seam is private same-crate cfg(test) — arming-style precedent only, not a public cross-crate feature API (authorized by C1 on its merits). MultiReplicaCluster is NOT unused (smoke-tested today). C16 is 9 clones + 1 delta, not 11. C19 covers 3 workers. "Feature-unification impossible" → "absent from release artifacts, machine-enforced". ~14 clients → 33 BFF fields + bespoke + domain + rules-client
sinks.

Verification

Every unit: its own solo-sequenced battery + the The never-easier checklist (created in C1; applied in every J-review) checklist in each J-review (zero nextest.toml serialization/envelope hunks unless semantically required and called out). C10 proves the two-paths rule live. C12 oracles reviewed for hidden ordering assumptions. C23 baselines start exact (every surface enumerated, never blanket-blessed). C24 verifies a WORKING SYSTEM (no-skip validate with fault required, every armed fault fired, panic-cleanup self-test, DSN refusal, loopback runtime check, artifact retention) — not merged-MR counting. Program exit = C24 merged (the B5 trigger).

Risk register

  • Trigger/DDL cleanup — explicit disarm() + scratch-DB backstop.

  • async-cleanup-in-Dropfinish() contract; Drop detects-only.

  • Cross-suite proxy deletion — run-scoped names + lease-aware stale-only sweep.

  • Port-pool exhaustion/abandonment — bounded pool, fail-loud with holders, TTL reclaim.

  • Control-plane exposure — loopback + isolation + non-root + preflight runtime check.

  • Feature leakage into release artifacts — graph rules + cargo-tree gate.

  • Stranded devstack overlay — lifecycle lock + dirty marker + auto-reconciliation + signal cleanup + post-restore health.

  • Abandoned compose projects — label+age sweep, stage-owned teardown.

  • Non-reproducible burst failures — mandatory seed + replay command + retained artifacts.

  • Secondary-Drop masking — armers assert only via explicit finish().

  • Early real-bug triage budget; wall-time creep vs ceilings.

  • toxiproxy outside cargo-deny — rides the pinned-image SBOM path.

Documentation Updates

  • docs/modules/ROOT/nav.adoc — this plan under Plans → Active (C0).

  • Epic &83 body — clickable plan link + child list (C0 link; C2 fills the child issues).

  • ADR-067 (two-layer fault-injection doctrine) + the contested-environment arbitration runbook (operations/contested-environment.adoc) (C1).

  • testing-reference.adoc — the never-easier checklist + a pointer to the arbitration ladder (C1).

  • CHANGELOG.adoc == Unreleased — per unit as tooling/CLI surfaces land.

Edit this page · latest