Plan: Tier 1 Review Remediation (2026-04-19)

On this page

Status

Step Description Status

1

Plan file, nav link, GitLab issues (MR !96)

Done (pre-ADR-030)

2

GitLab hygiene sweep (labels + milestones) — no branch, #188

Done (pre-ADR-030)

3

Code cleanup: CRAIG_SERVICES dedup + unwrap fix + swallowed-error log (MR !97, #189)

Done (pre-ADR-030)

4

Unit tests — intake unique-code retry (MR !98, #190)

Done (pre-ADR-030)

5

Unit tests — craig-api bootstrap helpers (MR !99, #191)

Done (pre-ADR-030)

6

Unit tests — xtask helpers (devstack + check_docs + check_preflight + ports_env) (MR !100, #192)

Done (pre-ADR-030)

7

E2E — cookie session lifecycle (ADR-013 verification) (MR !101, #193)

Done (pre-ADR-030)

8

E2E — concurrent unique-code submits + empty-state paths (MR !102, #194)

Done (pre-ADR-030)

9

Docs reconciliation + plan completion audit (this MR, #195)

Done (pre-ADR-030)

Epic: none (child issues grouped by milestone instead)
Issues: Step 2 → #188 · Step 3 → #189 · Step 4 → #190 · Step 5 → #191 · Step 6 → #192 · Step 7 → #193 · Step 8 → #194 · Step 9 → #195
Branches: one chore/, fix/, or test/ branch per step — see each step for naming
Milestone: %1 (2026 Q2 — Close Active Plans)

Context

A comprehensive 8-agent review of the 2026-04-18→19 Tier 1 sprint (14 merged MRs, ~1000 LOC: cookie-session migration, devstack module extraction, intake retry loop, bootstrap primitives, seed determinism, workflow rules) found zero critical correctness or security issues but surfaced major gaps in test coverage for just-landed code, one DRY violation, and some GitLab label/milestone hygiene misses.

Most of the new code was verified end-to-end via the pre-push hook but lacks explicit unit-test coverage of error paths. The project standard per .claude/docs/testing.md is 100% coverage of happy AND sad paths — several functions with 3–5 branches currently have zero direct tests. Retry logic in particular has 4 outcomes (no-collision, collision-then-success, retries-exhausted, non-retriable) — only the first is implicitly exercised by existing integration tests.

This plan closes those gaps, consolidates a duplicated service-list constant, reconciles a minor doc-count mismatch, and fixes GitLab metadata. Upon completion, the sprint’s deliverables will meet the project’s stated quality bar.

Scope

In scope:

  • Sad-path unit tests for all public functions landed 2026-04-18→19

  • E2E tests for the cookie-session lifecycle, concurrent submit, and empty-state UI paths

  • Extract CRAIG_SERVICES into one shared constant

  • Fix unwrap() in xtask/src/cmd/check_docs.rs:110 and swallowed check_staleness() error in xtask/src/devstack.rs:94

  • Backfill labels on !81 and !83; add milestone %1 to 7 closed issues

  • Reconcile CLAUDE.md vs services.md endpoint-count mismatch

Out of scope (file as separate issues if pursued):

  • CSRF sync-token pattern (beyond SameSite=Lax — future ADR)

  • Widening confirmation code suffix 4 → 8 hex (retry loop handles collisions; optimization only)

  • Intake AppState consolidation (pre-existing pattern, not this sprint’s regression)

  • cargo-audit allow_failure policy review (needs separate decision on transitive advisories)

Design

Testing strategy

Retry-loop tests (Step 4): the current implementation has the retry inline in ReportSink::Database::accept. Extract it into a module-private helper insert_with_unique_retry(max_retries, mut try_insert) where try_insert: FnMut() → Future<Result<T, sqlx::Error>>. Unit tests pass a closure with a counter returning fake sqlx::Error::Database(MockDbError) N times then Ok. No public API change. Mock the sqlx::error::DatabaseError trait with a test-only struct returning code() == "23505" for unique violations.

Bootstrap helper tests (Step 5): point the new helpers at unreachable addresses (127.0.0.1:1) under tokio::time::timeout to force the error branch. Context messages pinned via to_string().contains("failed to connect to …​").

Xtask helper tests (Step 6):

  • CheckDocsOutcome variants: factor out the URL/file sha256 dependencies behind a trait or test-only function so fixtures can drive all three variants.

  • check_preflight: refactor into check_preflight_with_probes(nextest_probe, docker_probe) where probes are Fn() → Result<()> closures injectable from tests.

  • devstack::ensure_ready dispatch: factor into a pure decide_action(force, markers_exist, staleness) → StartAction (new enum) that’s 100% unit-testable. IO stays in ensure_ready itself.

  • ports_env_overrides: use tempfile::tempdir() to build fake workspace directories.

E2E strategy (Steps 7–8): add cargo xtask dev restart-service <name> subcommand (thin wrapper around docker::compose(["restart", name]) + a single-service health wait) so the cookie-lifecycle spec can restart craig-web without the spec shelling out directly (that would violate the "NEVER run docker compose directly" rule in coding-conventions.md). Playwright helper restartCraigWebAndWait() in tests/e2e/lib/helpers.ts invokes it.

Shared CRAIG_SERVICES constant

Currently duplicated in xtask/src/devstack.rs:21 and xtask/src/cmd/dev.rs:10 (both private, both 9 entries, identical). Target: one pub const CRAIG_SERVICES: &[&str] in xtask/src/docker.rs (already owns PORT_MAPPINGS — the service-name source of truth). Both call sites import from there.

Steps

Each step is one logical unit of work shippable as its own MR. Steps are largely independent and can parallelize.

Files: docs/modules/ROOT/pages/plans/tier-1-review-remediation.adoc (this file), docs/modules/ROOT/nav.adoc

Branch: chore/plan-tier-1-review-remediation

  1. Write this plan as .adoc at the path above.

  2. Add to nav.adoc under Active: * Tier 1 Review Remediation

  3. Create a parent GitLab epic (or reuse an existing one linked to milestone %1).

  4. Create one GitLab issue per remaining step (Steps 2–9). Each:

    • Title prefixed with type (chore:, test:, fix:)

    • Description linking back to this plan

    • Labels: test for Steps 4–8; chore for Steps 2, 3, 9; P3-low default; service labels (craig-intake, craig-web) where applicable

    • Milestone %1 (2026 Q2 — Close Active Plans)

Verification: cargo xtask check-docs passes. glab issue list --milestone %1 shows the new issues.

Step 2: GitLab hygiene sweep

No code changes.

Branch: none (metadata-only via glab)

  1. glab mr update 81 --label "feat,craig-web,P2-medium"

  2. glab mr update 83 --label "docs"

  3. For each of #176, #182, #183, #184, #185, #186, #187: glab issue update N --milestone "2026 Q2 — Close Active Plans" (verify exact name via glab api …​/milestones?state=active)

Verification: glab mr view 81 | grep labels and glab mr view 83 | grep labels show the new labels. Each of the 7 issues' milestone field populated.

Step 3: Code cleanup — CRAIG_SERVICES dedup + minor nits

Files: xtask/src/docker.rs, xtask/src/devstack.rs, xtask/src/cmd/dev.rs, xtask/src/cmd/check_docs.rs

Branch: chore/craig-services-dedup

  1. Add pub const CRAIG_SERVICES: &[&str] = &[…​] to docker.rs (9 entries, match the existing).

  2. Remove local CRAIG_SERVICES from devstack.rs:21 and cmd/dev.rs:10; replace uses with crate::docker::CRAIG_SERVICES.

  3. In check_docs.rs:110 replace child.stdin.as_mut().unwrap().write_all(&curl.stdout)?; with child.stdin.as_mut().context("sha256sum stdin not available")?.write_all(&curl.stdout)?; (add anyhow::Context to imports if missing).

  4. In devstack.rs:94 Err branch of check_staleness, log the error before falling through: tracing::warn!(error = %e, "staleness check failed — doing cold start");.

Verification: cargo build --workspace --locked, cargo fmt --check --all, cargo clippy --workspace --locked — -D warnings. cargo xtask dev status still renders correctly (uses new import path).

Step 4: Unit tests — intake unique-code retry

Files: services/craig-intake/src/sink.rs (extract helper + add #[cfg(test)] mod retry_tests)

Branch: test/intake-retry-unit-coverage

  1. Extract helper (module-private):

    async fn insert_with_unique_retry<F, Fut, T>(
        max_retries: usize,
        mut try_insert: F,
    ) -> Result<T, sqlx::Error>
    where
        F: FnMut() -> Fut,
        Fut: std::future::Future<Output = Result<T, sqlx::Error>>,
    {
        let mut last_err: Option<sqlx::Error> = None;
        for attempt in 0..max_retries {
            match try_insert().await {
                Ok(v) => return Ok(v),
                Err(sqlx::Error::Database(db_err)) if db_err.is_unique_violation() => {
                    tracing::warn!(attempt = attempt + 1, constraint = db_err.constraint(),
                        "unique violation — retrying");
                    last_err = Some(sqlx::Error::Database(db_err));
                    continue;
                }
                Err(e) => return Err(e),
            }
        }
        Err(last_err.unwrap_or_else(|| sqlx::Error::Protocol("retries exhausted".into())))
    }
  2. Refactor ReportSink::Database::accept retry loop to call insert_with_unique_retry(5, || store::reports::create_report(…​input.clone())). Closure regenerates id/confirmation_code per invocation.

  3. Add four tests:

    • no_collision_returns_first_result — closure returns Ok(()) immediately.

    • one_collision_then_success — counter returns unique-violation once, then Ok.

    • retries_exhausted_returns_last_err — counter returns unique-violation 5 times; assert Err(Database(_)) with is_unique_violation().

    • non_retriable_error_returns_immediately — closure returns sqlx::Error::RowNotFound; assert counter == 1 (no retries).

  4. Implement MockDbError test struct implementing sqlx::error::DatabaseError with code() → "23505" and constraint() → Some("public_reports_confirmation_code_key").

Verification: cargo nextest run -p craig-intake --lib — 4 new tests pass. Integration test count (117) unchanged.

Step 5: Unit tests — craig-api bootstrap helpers

Files: crates/craig-api/src/bootstrap.rs (expand #[cfg(test)] mod tests)

Branch: test/craig-api-bootstrap-coverage

  1. connect_database_invalid_url_returns_errconnect_database("postgres://x:y@127.0.0.1:1/db", 1, Duration::from_millis(100)) wrapped in tokio::time::timeout(Duration::from_secs(2), …​); assert Err containing "failed to connect to database".

  2. init_auth_unreachable_issuer_returns_errinit_auth("http://127.0.0.1:1/realms/none", None); assert Err containing "failed to fetch JWKS from Keycloak".

  3. connect_mq_invalid_url_returns_errconnect_mq("amqp://invalid:1/%2f"); assert Err containing "failed to connect to RabbitMQ".

  4. Strengthen bootstrap_result_fields_are_public — instantiate a BootstrapResult from zeroed defaults (or equivalent) so the test actually exercises the struct rather than just compiling a field-access closure.

Verification: cargo nextest run -p craig-api — 4 tests pass (3 new + 1 strengthened).

Step 6: Unit tests — xtask helpers

Files: xtask/src/cmd/check_docs.rs, xtask/src/cmd/validate.rs, xtask/src/devstack.rs, crates/craig-test-lib/src/config.rs (each adds #[cfg(test)] mod tests)

Branch: test/xtask-unit-coverage

  1. check_docs — factor out sha256_url and download_file behind a trait or pub(crate) fn that tests can stub. Three tests drive all three CheckDocsOutcome variants:

    • all_match_returns_allmatch

    • drift_reports_issues_drifted

    • missing_reports_issues_missing

  2. validate::check_preflight — refactor to check_preflight_with_probes(skip_docker, nextest_probe, docker_probe) where probes are impl Fn() → Result<()>. Two tests:

    • nextest_missing_returns_err_with_install_hint

    • docker_daemon_unreachable_returns_err_with_startup_hint

  3. devstack — factor dispatch logic into a pure decide_action(force, markers_exist, staleness_result) → StartAction enum (ColdStart, AlreadyCurrent, ContainersDown, CachedRebuild, NoCacheRebuild, VolumeWipe). Unit-test all 6 variants. IO in ensure_ready stays.

  4. craig-test-lib::ports_env_overrides — tests with tempfile::tempdir():

    • workspace_with_ports_env_parsed — all entries returned

    • no_ports_env_returns_empty_map

    • malformed_lines_skipped — comments / blank / no-= / empty-key all skipped

    • non_workspace_cargo_toml_keeps_walkingCargo.toml without [workspace] isn’t treated as root

    Use serial_test or OnceLock reset pattern for parallel-safety of the cache.

Verification: cargo nextest run --workspace --lib — ~13 new tests pass across check_docs / validate / devstack / test-lib.

Files: tests/e2e/specs/auth-cookies.spec.ts (new), tests/e2e/lib/helpers.ts (add restartCraigWebAndWait), xtask/src/cmd/dev.rs (add DevCommand::RestartService { name: String })

Branch: test/e2e-cookie-session-lifecycle

  1. Add cargo xtask dev restart-service <name>:

    • Thin wrapper: docker::compose(&["restart", &name])?; then a single-service health wait (either update wait_for_health to accept a filter or write wait_for_one(svc))

    • Validate name against crate::docker::CRAIG_SERVICES to prevent arbitrary docker-compose exec

  2. Add tests/e2e/lib/helpers.ts::restartCraigWebAndWait() — shells out to cargo xtask dev restart-service craig-web via child_process.execFileSync and returns after the devstack health-check clears.

  3. Add auth-cookies.spec.ts with four tests:

    • session cookie survives craig-web restart — log in (use [caseworker] storage state), invoke helper, page.goto('/'), assert no redirect to Keycloak and dashboard renders.

    • tampered session cookie redirects to login — manipulate craig-session value via context.addCookies, navigate to protected page, expect 302 to Keycloak auth URL.

    • expired session cookie redirects to login — override cookie with expires in the past, navigate, expect redirect.

    • logout clears session cookie — click logout, assert Set-Cookie: craig-session=; Max-Age=0 in response headers (or cookie absent from subsequent requests), navigate to protected page, expect redirect.

Verification: cargo xtask e2e — --grep "session cookie" — 4 tests pass.

Step 8: E2E — concurrent unique-code submits + empty-state paths

Files: tests/e2e/specs/public-report.spec.ts (extend), tests/e2e/specs/placement.spec.ts (extend), tests/e2e/specs/exchange.spec.ts (extend)

Branch: test/e2e-concurrent-and-empty-states

  1. concurrent reports receive distinct confirmation codes (public-report.spec.ts): use Promise.all([…​5 submits…​]), collect confirmation codes from responses, assert all 5 unique and match /^RPT-\d{8}-[A-F0-9]{4}$/. Proves !92’s retry loop works under parallel load.

  2. empty case dropdown on /placement/new shows no-data state (placement.spec.ts): navigate as a user predicted to have 0 open cases. If seed provides at least one such case for the chosen user, test.skip() with a clear comment. Otherwise assert dropdown has only the placeholder option or shows a "No cases assigned" message.

  3. partner with no agreements shows empty state (exchange.spec.ts): use the fixture partner created by the existing create exchange partner test (which starts with zero agreements) or create fresh, navigate to its detail page, assert empty-state copy instead of a table or error.

Verification: cargo xtask e2e — 145 + 3 new tests pass. Full pre-push battery green.

Step 9: Docs reconciliation + plan completion audit

Files: .claude/docs/services.md OR .claude/CLAUDE.md, docs/modules/ROOT/pages/plans/tier-1-review-remediation.adoc (this file, status updates), docs/modules/ROOT/pages/plans/archive.adoc (add archive row), docs/modules/ROOT/nav.adoc (Active → Archive)

Branch: chore/plan-tier-1-completion-audit

  1. Reconcile Rules Engine endpoint count: count actual #[utoipa::path] occurrences in services/craig-rules/src/api/ and update whichever doc diverges (CLAUDE.md says 9; services.md says 10). Scan other Phase Status rows while touching CLAUDE.md.

  2. Run Plan Completion Audit per .claude/docs/delivery-protocol.md — spawn a subagent to verify every acceptance criterion + doc update + issue closure with commit SHAs.

  3. Update this plan’s Status table — all steps Complete.

  4. Move nav entry from Active → Archive; add row to archive.adoc under Code Quality.

Verification: cargo xtask check-docs clean. Plan Completion Audit reports zero residual findings.

Files Touched

File Change

xtask/src/docker.rs

Add pub const CRAIG_SERVICES (Step 3)

xtask/src/devstack.rs

Remove local const; factor decide_action(); log staleness err (Steps 3, 6)

xtask/src/cmd/dev.rs

Remove local const; add DevCommand::RestartService (Steps 3, 7)

xtask/src/cmd/check_docs.rs

unwrapcontext?; add test module (Steps 3, 6)

xtask/src/cmd/validate.rs

Factor check_preflight_with_probes; add test module (Step 6)

crates/craig-api/src/bootstrap.rs

Expand sad-path test coverage (Step 5)

crates/craig-test-lib/src/config.rs

Add test module for ports_env_overrides (Step 6)

services/craig-intake/src/sink.rs

Extract insert_with_unique_retry; add retry tests (Step 4)

tests/e2e/specs/auth-cookies.spec.ts

New — cookie lifecycle tests (Step 7)

tests/e2e/specs/public-report.spec.ts

Add concurrent-submission test (Step 8)

tests/e2e/specs/placement.spec.ts

Add empty-dropdown test (Step 8)

tests/e2e/specs/exchange.spec.ts

Add empty-agreements test (Step 8)

tests/e2e/lib/helpers.ts

Add restartCraigWebAndWait() (Step 7)

docs/modules/ROOT/pages/plans/tier-1-review-remediation.adoc

This plan (Step 1) + status updates on completion (Step 9)

docs/modules/ROOT/nav.adoc

Active entry (Step 1) → Archive on completion (Step 9)

docs/modules/ROOT/pages/plans/archive.adoc

Archive row on completion (Step 9)

.claude/docs/services.md or .claude/CLAUDE.md

Endpoint count fix (Step 9)

.claude/docs/testing.md

Document new xtask dev restart-service subcommand (Step 7)

CHANGELOG.adoc

Entry under == Unreleased (Steps 4–8 as they land)

Verification

  1. cargo nextest run --workspace --lib — ~21 new unit tests pass (4 intake retry + 4 bootstrap + ~13 xtask/test-lib)

  2. cargo xtask dev reseed

  3. cargo nextest run --workspace --locked --profile integration — 1182 + new tests pass

  4. cargo xtask e2e — 145 + 7 new tests pass (4 cookie + 1 concurrent + 1 empty-dropdown + 1 empty-agreements)

  5. Full cargo xtask validate + pre-push gate green

  6. cargo xtask check-docs clean

  7. glab issue list --milestone "2026 Q2 — Close Active Plans" shows all 7 previously-unassigned issues attached

  8. glab mr view 81 | grep labels and glab mr view 83 | grep labels show new labels

  9. Plan Completion Audit subagent — zero residual findings

Documentation Updates

  • .claude/docs/services.md — endpoint count reconciliation (Step 9)

  • .claude/docs/testing.md — document new xtask dev restart-service subcommand (Step 7)

  • CHANGELOG.adoc — entry under == Unreleased: "Added sad-path unit tests for session bootstrap, intake retry, xtask helpers; E2E for cookie session lifecycle"

  • docs/modules/ROOT/pages/plans/archive.adoc — archive row on completion (Step 9)

  • docs/modules/ROOT/nav.adoc — move plan Active → Archive on completion (Step 9)

Errata

Step Deviation

3

Plan specified tracing::warn! for the check_staleness error log in devstack.rs. The xtask crate has no tracing dependency — it’s a pure CLI task runner that uses eprintln! for all diagnostics. Matched existing style with eprintln!(" WARN: staleness check failed ({e}) — doing cold start") instead of adding a new dependency for one log line.

7

Plan specified that restartCraigWebAndWait() invoke cargo xtask dev restart-service craig-web via child_process.execFileSync. The Playwright container (mcr.microsoft.com/playwright:v1.58.2-noble) has no Rust toolchain on PATH, so execFileSync('cargo', …​) fails with ENOENT. Switched the helper to POST directly to the Docker daemon’s REST API over the Unix socket (now mounted into craig-e2e rw in docker-compose.yml) — same action the subcommand performs (docker restart), just reached through the daemon. The cargo xtask dev restart-service subcommand itself is still the primary interface for humans and pre-push hooks.

Edit this page · latest