Plan: Standalone-SHINES Intake Test-Coverage Remediation

On this page

This plan remediates the test-coverage gaps found by an exhaustive audit of the standalone-SHINES intake path (the epic &58 / ADR-042 deliverable). The audit ran 12 per-component auditors (each sweeping happy/sad/evil/chaos × unit/integration/e2e) plus an evil/chaos completeness critic and a dedup/false-positive critic, producing 258 raw gap cells that collapse to roughly 50–70 concrete test functions + one fault-injection harness + one pre-push gate change + one design call.

Status

Wave Description Status

0

Fault-injection harness (craig-mock-server SHINES faults) + CRAIG→SHINES idempotency design decision

Done (2026-06-25) — !799

1

Sad/happy unit fills — mapper father-symmetry + parametric RefVal arms + multi-person routing + edges; crosswalk/narrative/fidelity

Done (2026-06-25) — !800

2

Evil corpus — parametric malformed-JWS + kid-enumeration uniformity (signed path); per-field injection corpus; SSN-unicode; PII-in-errors

Done (2026-06-25) — !801

3

Chaos — SHINES + keyring fault propagation; register-proxy relay; keyring concurrency/crash/restart; rate-limit burst; route/profile visibility

Done (2026-06-25) — !803

4

Process — bake SDK suites (pytest + vitest) into the pre-push cargo xtask validate gate; fix the #700 Python signer first

Done (2026-06-25) — !804

5

E2E top-ups — SHINES-mock-5xx surfaces a redacted form error; duplicate-kid 409 at keygen; document the secure-context keygen limit

Done (2026-06-25) — !805

Epic: &59
Issues: #701 (W0) / #702 (W1) / #703 (W2) / #704 (W3) / #705 (W4) / #706 (W5); W4 relates #698 (SDK gating) + #700 (Python signer)
Scope depth: literal 100% — every audited cell at P1, P2, and P3
Branch (per wave): feature/intake-coverage-*

Context

The epic &58 runtime is functionally complete and proven against the SHINES mock + the real keyring sidecar, but the audit found coverage that is strong on unit happy/sad, thin on evil at the integration layer, and nearly absent on chaos (fault-injection) everywhere. Gap distribution: chaos 95 · evil 71 · sad 58 · happy 34; by layer integration 140 · unit 115 · e2e 3. The most consequential finding is that no test injects a SHINES or keyring fault — a SHINES outage is a likely production event and its handling is unverified end-to-end.

This is a test-and-resilience plan, not a feature plan. It does not resolve the external go-live data dependencies (#691 SHINES API contract, #692 RefVals, #693 required-field set) — those remain mocked/placeholder by design. It DOES add the test infrastructure (a fault-injection mock mode) those resilience tests need.

Scope

In scope: unit + integration + e2e tests across all four axes for: the CpsRequest mapper, the RefVals crosswalk, ExtraFields/NarrativeView parse+sanitize, the boot fidelity gate, ShinesSink, the signed-submit handler + register proxy + shared JWS verify, validation/Channel/relevance, config+boot+router, the craig-intake-keyring sidecar, and the three SDKs; a craig-mock-server SHINES fault-injection mode; baking the SDK suites into the pre-push gate.

Out of scope: the real SHINES API contract / RefVals / required-field set (#691/#692/#693); SHINES-outage buffering (#215 local-outbox — a feature, not a test); mutation-testing depth (tracked separately); any non-intake service.

Design

D1. Axes + layers (the coverage contract)

  • happy — valid input → correct success + correct output value/shape.

  • sad — well-formed but invalid / business-rejected input → correct typed 4xx (validation, required-when-relevant, reporter-type channel, enum rejection, missing/duplicate parent, unmappable RefVal).

  • evil — adversarial: injection (HTML/script/control-char/unicode), oversized/DoS, malformed/forged/replayed/ expired JWS, auth bypass, kid enumeration, PII in errors/logs, secret leakage.

  • chaos — fault-injection/resilience: upstream (SHINES/keyring) outage/timeout/5xx/non-JSON, concurrency/races, crash-recovery + atomic-write durability, cache staleness, restart/replay, clock-skew/iat window, rate-limit under load, route/profile visibility.

  • Layers: unit (in-crate #[cfg(test)] / SDK unit), integration (devstack-gated tests/ over real HTTP), e2e (Playwright). E2E stays deliberately thin — the HTTP-level integration tests carry the load; the browser keygen’s crypto.subtle needs a secure context, which host.docker.internal is not, capping e2e crypto.

D2. The fault-injection harness (Wave 0 — prerequisite for Wave 3)

tools/craig-mock-server/src/shines.rs currently always returns 200 {record_id,status:"accepted"} — it cannot exercise any chaos path. Add a fault-injection mode so a test can make the mock return a configured fault: a Fault enum (Status(u16), Timeout/hang, NonJsonBody, WrongStatusField, Malformed), selected per request via a header (e.g. X-Mock-Fault: status=500) or a spawn_for_test-configured state — mirroring the existing craig_mock_server::spawn_for_test() ephemeral-port harness. This unblocks every Wave-3 ShinesSink and (where the mock fronts the keyring) keyring-fault test. Keep the default behavior byte-identical (no fault header ⇒ the current 200 path), so existing tests are unaffected.

D3. CRAIG→SHINES idempotency (design decision, settled in Wave 0)

received_request_id is minted per submission but never carried onto the CRAIG→SHINES POST, so a future retry (once #215 buffering lands) could double-submit. Decision: there is no retry today, so no live bug; we document the idempotency requirement on the proposed contract (the CpsRequest gains an idempotency key when #691 is finalized + #215 retry is built) and add a regression test asserting the sink performs exactly one POST per forward() (no accidental retry). We do NOT add an idempotency key to the wire now (that belongs with #691/#215). Recorded as an errata/decision note in the proposed-API spec.

D4. SDK suites in the pre-push gate (Wave 4 — re-scopes #698)

CI is unreliable (recurring 2xlarge disk OOM), so the pre-push cargo xtask validate is the canonical functional gate — the SDK suites belong there, not (only) in CI. This box has no host node/npm/python toolchain, so the SDK steps run in Docker (raw docker run, consistent with the e2e pattern): a python:alpine image for pytest sdks/python/tests and the existing Playwright/node image for vitest sdks/typescript/tests, added as a new cargo xtask validate step (after the nextest step, before/with the docker-build step), gated (blocking) and skippable under --skip-docker like the other Docker-dependent steps. Sequencing: the #700 Python-signer fix lands FIRST (else the Python signed tests fail the new gate), then the gate is added with the Python signed-submit test re-enabled.

D5. Parametric corpora (evil)

Evil coverage uses parametric/table tests (the project’s evil-input-corpus pattern), not one-off cases: a malformed-JWS corpus (alg=none / HS256 confusion / 2-segment / bad-base64 header·sig / missing jti·iat / tampered payload / forged sig / wrong key / expired·future iat) all asserting a uniform bare 401 (kid-enumeration defense); and an injection corpus over all 10 ExtraFields + 9 NarrativeView free-text fields (HTML/script/control-char/unicode) asserting strip-before-forward, with the SSN field asserting NO transform (J4). Per-field breadth is P2/P3 defense-in-depth (the sanitize mechanism is already unit-tested); literal-100% includes it.

Waves

Each wave is one issue → one MR under the standard cadence (branch off fresh main → implement → dev reload where server-behavior/devstack-gated tests are added → full cargo xtask validate → fresh-eyes J1–J8 over the staged diff → skip-CI merge → close issue → prune). Every new test carries an // @axis: tag.

Wave 0 (harness + decision)

Files: tools/craig-mock-server/src/shines.rs (+ lib.rs if the Fault config surfaces through spawn_for_test). Per D2 + D3. Verify: the mock returns each configured fault; default path unchanged (existing ShinesSink round-trip test still green); the idempotency decision recorded in interfaces/shines-cps-intake-proposed-api.adoc + a sink "exactly-one-POST" unit test.

Wave 1 (sad/happy unit fills)

Files: services/craig-intake/src/backend/shines/mapper.rs (#[cfg(test)]), crates/craig-reference/src/shines.rs, backend/shines/{narrative.rs,fidelity.rs}. Concrete tests: duplicate_father + missing_required_father (symmetry with mother); a parametric unmappable_refval table over marital/language/gender/reporter-type; multiple_children_all_mapped + multiple_other_household_members; incident-date branches (datetime / date-only fallback / both-absent); compose_address edges (all-empty / partial / unicode); map_ssn edges (whitespace / all-zeros / unicode-digit→reject); child-no-gender; NarrativeView parse-all-9 + default-all-None; fidelity sentinel-detected-fails-closed. A mapper round-trip proptest (arbitrary valid submission → maps without panic, wire keys stable — the optional motherInfo/fatherInfo keys appear iff that slot is designated). The sentinel-detected-fails-closed test required a small testability extraction in fidelity.rs (a generic ensure_nonsentinel helper the crosswalk-totality check now delegates to — no behavior change). The crates/craig-reference/src/shines.rs fill is race / marital-status / reporter-type RefVal uniqueness invariants (parity with the existing language-uniqueness test; guards the #692 placeholder→authoritative swap). Verify: cargo nextest run -p craig-intake -p craig-reference.

Wave 2 (evil corpus)

Files: services/craig-intake/tests/api/signed_submit.rs (or a new signed_evil.rs), api/validation.rs tests, backend/shines/mapper.rs tests. Per D5: the parametric malformed-JWS corpus (uniform-401
kid-enumeration byte-identical-body assertion on the signed path); the per-field injection corpus over all ExtraFields + NarrativeView text (strip-before-forward; SSN untouched); SSN unicode-digit rejection (evil); PII-not-in-error assertions (malformed DOB / <script> name → value absent from the 4xx body). Verify: cargo nextest run -p craig-intake + --run-ignored all.

Layer choices (living-spec, as built): the malformed-JWS corpus lives in signed_submit.rs (reusing its keyring register_and_approve / post_signed helpers — no separate signed_evil.rs) as one parametric integration test firing 13 failure modes (2-segment / bad-base64 header·sig / alg=none / HS256 / missing jti·iat / expired·future iat / unknown-kid / wrong-key / forged-sig / tampered-payload) and asserting a uniform byte-identical 401 (the standalone-path analogue of jws_error_uniformity.rs). The per-field injection corpus is unit-level (validation.rs sanitize tests over all 10 ExtraFields + all 9 NarrativeView fields) — the mechanism is already integration-proven, so per-field breadth is fastest+total at the unit layer. SSN unicode-digit rejection is unit-locked in Wave 1 (mapper::map_ssn); Wave 2 adds the J4 invariant that the sanitize pass leaves ssn byte-identical (so the unicode digits reach the mapper intact). PII-not-in-error is a new pii_not_in_errors.rs integration test over the shared prepare_submission pipeline (public path).

Wave 3 (chaos — the resilience core; depends on Wave 0)

Files: services/craig-intake/tests/api/ (new chaos tests), backend/shines/sink.rs tests, services/craig-intake-keyring/tests/. Using the Wave-0 mock faults: ShinesSink — SHINES 4xx/5xx/timeout/non-JSON/status≠accepted → correct redacted 500 (no PII/body leak); signed submit — keyring lookup/replay 5xx/timeout → 500 not 401; register-proxy relay of keyring 5xx/timeout/ 409/429; signed-path pending/revoked-key cold-path → 401; iat too-old/too-future → 401 (signed path); rate-limit burst → 429 (signed + public); route/profile visibility (status route absent under Shines, /signed/v1 absent under None); keyring — concurrent register/approve races (exactly-one-wins, disk stays valid JSON), atomic-write crash safety (recovery test), replay-dedup across restart, max_keys 429, retention-window pruning. Verify: dev reloadcargo nextest run -p craig-intake -p craig-intake-keyring --run-ignored all.

As built (living-spec): much of this wave was already covered by earlier waves + the keyring’s own suite, so W3 adds only the genuine gaps. Already shipped: ShinesSink 5xx/timeout/non-JSON/status≠accepted→500 (W0); iat too-old/too-future→401 (W2 corpus); the keyring store’s concurrency / atomic-write-no-residue / reload-across- restart / max_keys / retention-pruning / replay-dedup (its existing store.rs suite). Discovered + fixed first (own MR, user-directed): the keyring-lookup-outage→401 bug → #707 (MR !802) corrected it to 500; the "keyring lookup 5xx→500" AC is satisfied by #707’s unit tests. W3 net-new: ShinesSink 4xx redaction (forward_redacts_shines_4xx_without_leaking_body — a 400→400, a non-semantic 404→500, body redacted both — the AC’s "4xx→500" holds only for non-semantic codes; a 400 is a client-rejection→400 per redact_upstream_error’s HTTP semantics); the replay outage path (`JwsReplayClient transport-classification unit test
verify_signed_jws’s visible Transport→500 map); register-proxy relay status mapping (`map_keyring_register_error unit: 400/409/429 relayed, 5xx redacted→500); pending + revoked cold-path→401 (live-keyring integration, no-prior-lookup so the 60s signer cache is never populated); route/profile visibility (signed route 401-on-Shines / 404-on-None; status route bare-404-on-Shines / ProblemDetails-404-on-None); keyring exactly-one-wins on a concurrent same-kid register race. Test mechanism (open-question resolution): keyring/registry faults use the unreachable-SignerAuthClient/JwsReplayClient + verify_signed_jws unit pattern (the dummy_clients seam) — no craig-mock-server keyring routes / wiremock. Deferred (tracked): the public/signed HTTP-level 429 burst is unit-covered (rate_limiter_throttles_burst_per_ip) but the HTTP-path assertion is deferred to #708 — an IP-keyed limiter on a shared devstack instance would poison sibling tests; the per-partner HTTP-429 is already in per_partner_rate_limit.rs.

Wave 4 (process — SDK suites in the pre-push gate; re-scopes #698)

Files: sdks/python/ (the #700 signer fix + re-enable the signed-submit test), xtask/src/cmd/validate.rs (+ docker.rs helpers) for the Docker-run pytest+vitest step, .githooks/ if needed. Per D4. Sequencing: #700 first, then the gate. Verify: cargo xtask validate runs both SDK suites (Docker) and fails on a red suite; --skip-docker skips them; a deliberately-broken SDK test is caught by the gate.

As built (living-spec): #700 had two joserfc>=1.0 breakages, both masked because the suite was not in CI: (1) the strict header registry rejected the required jti/iat protected headers — fixed by passing an extended JWSRegistry (registers jti/iat, pins algorithms=["ES256"]) to serialize_compact; (2) the test keypair helpers used the pre-1.0 ECKey.as_dict(is_private=…) kwarg, silently swallowed under 1.x (it exported a public key, no d) — renamed to as_dict(private=…). The deferred happy-path lockstep (test_submit_report_signed_sends_jws_no_apikey) now lands. The gate is a new cargo xtask sdk-test subcommand (the #698-anticipated entrypoint) running pytest in python:3.13-alpine + vitest in node:22-alpine; the per-suite Docker mechanism (docker::run_sdk_suite) bind-mounts sdks/ read-only and copies it into the container so the shared sdks/test-vectors/ relative path resolves and the host tree stays pristine. It is folded into validate (a [11b/13] phase, skipped under --skip-docker). Correction to the §D4 assumption: the pre-push hook runs validate --skip-docker (it skips the slow compose build; e2e covers Docker), so the canonical pre-push coverage comes from a separate cargo xtask sdk-test step added to the hook’s project-specific region (outside the managed markers; SKIP_SDK=1 opt-out), not from the in-validate phase. #698 is thereby satisfied (CI → pre-push) and closed by this MR.

Wave 5 (e2e top-ups)

Files: tests/e2e/specs/intake-keygen-signed.spec.ts + intake-embedded-ui-shines.spec.ts, tools/craig-mock-server/src/shines.rs (devstack-reachable fault toggle if needed). A SHINES-mock-5xx run that asserts the form surfaces a redacted error (not a leak); a duplicate-kid 409 at /keygen; a spec comment
a one-line doc documenting the crypto.subtle secure-context limitation. Verify: cargo xtask e2e --no-refresh — --project=intake-ui-shines.

As built (living-spec): the "devstack-reachable fault toggle" is a one-shot fault-injection control endpoint on the host-owned SHINES mock — POST /shines/_control/fault (body {status}, default 503) arms the next cps-request to return that status with a deliberately leaky sentinel body, then auto-clears; DELETE clears an unconsumed fault. This is deliberately separate from the crate’s existing fault module (a spawn-time, every-route layer for in-process adapter tests — not runtime-toggleable over HTTP). One-shot + auto-clear makes it poison-free; combined with the e2e’s workers: 1 serial execution + a finally clear, it can’t leak into a sibling spec (so this wave did NOT need the deferred-#708 isolation). A new CRAIG_SHINES_MOCK_URL is derived in xtask (build_env_for_ports + write_ports_env, mirroring CRAIG_INTAKE_KEYRING_URL) and added to the craig-e2e compose env so the Playwright container reaches the mock. The redaction proof is the absence of the mock’s leak sentinel from the form’s .errors-box. The dup-kid 409 + the secure-context limitation are HTTP-level/doc top-ups (the secure-context note lives on the keyring page, the constraint already governed #690’s HTTP-level approach).

Verification (per wave)

cargo fmt --all; cargo xtask validate (fmt/clippy/budgets/axis/machete/check-docs/plan-lint + nextest --run-ignored=all against the live devstack — dev reload first for any devstack-gated additions); a fresh Explore subagent answers J1–J8 over the staged diff. The program is complete when every audited cell is covered (or explicitly waived with a recorded reason) and cargo xtask validate is green with the SDK suites gated.

Appendix — audit provenance

Source: a 14-agent coverage workflow (2026-06-25) — 12 per-component auditors + an evil/chaos completeness critic + a dedup/false-positive critic (which rated the raw findings ~85% trustworthy and removed 3 false positives: the sanitize-mechanism, NarrativeView-integration-coverage, and Predicate::Not cells). Raw totals: 258 gap cells; chaos 95 / evil 71 / sad 58 / happy 34; integration 140 / unit 115 / e2e 3; effort S 108 / M 128 / L 22. The waves above are the de-duplicated, severity-normalized distillation.

Edit this page · latest