Plan: Tier 1 Review Remediation (2026-04-19)
On this page
- Status
- Context
- Scope
- Design
- Steps
- Step 1: Plan file + nav link + GitLab issues
- Step 2: GitLab hygiene sweep
- Step 3: Code cleanup —
CRAIG_SERVICESdedup + minor nits - Step 4: Unit tests — intake unique-code retry
- Step 5: Unit tests — craig-api bootstrap helpers
- Step 6: Unit tests — xtask helpers
- Step 7: E2E — cookie session lifecycle (ADR-013 verification)
- Step 8: E2E — concurrent unique-code submits + empty-state paths
- Step 9: Docs reconciliation + plan completion audit
- Files Touched
- Verification
- Documentation Updates
- Errata
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: |
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_SERVICESinto one shared constant -
Fix
unwrap()inxtask/src/cmd/check_docs.rs:110and swallowedcheck_staleness()error inxtask/src/devstack.rs:94 -
Backfill labels on !81 and !83; add milestone
%1to 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
AppStateconsolidation (pre-existing pattern, not this sprint’s regression) -
cargo-auditallow_failurepolicy 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):
-
CheckDocsOutcomevariants: factor out the URL/file sha256 dependencies behind a trait or test-only function so fixtures can drive all three variants. -
check_preflight: refactor intocheck_preflight_with_probes(nextest_probe, docker_probe)where probes areFn() → Result<()>closures injectable from tests. -
devstack::ensure_readydispatch: factor into a puredecide_action(force, markers_exist, staleness) → StartAction(new enum) that’s 100% unit-testable. IO stays inensure_readyitself. -
ports_env_overrides: usetempfile::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.
Step 1: Plan file + nav link + GitLab issues
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
-
Write this plan as
.adocat the path above. -
Add to
nav.adocunderActive:* Tier 1 Review Remediation -
Create a parent GitLab epic (or reuse an existing one linked to milestone
%1). -
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:
testfor Steps 4–8;chorefor Steps 2, 3, 9;P3-lowdefault; 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)
-
glab mr update 81 --label "feat,craig-web,P2-medium" -
glab mr update 83 --label "docs" -
For each of
#176, #182, #183, #184, #185, #186, #187:glab issue update N --milestone "2026 Q2 — Close Active Plans"(verify exact name viaglab 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
-
Add
pub const CRAIG_SERVICES: &[&str] = &[…]todocker.rs(9 entries, match the existing). -
Remove local
CRAIG_SERVICESfromdevstack.rs:21andcmd/dev.rs:10; replace uses withcrate::docker::CRAIG_SERVICES. -
In
check_docs.rs:110replacechild.stdin.as_mut().unwrap().write_all(&curl.stdout)?;withchild.stdin.as_mut().context("sha256sum stdin not available")?.write_all(&curl.stdout)?;(addanyhow::Contextto imports if missing). -
In
devstack.rs:94Errbranch ofcheck_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
-
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()))) } -
Refactor
ReportSink::Database::acceptretry loop to callinsert_with_unique_retry(5, || store::reports::create_report(…input.clone())). Closure regeneratesid/confirmation_codeper invocation. -
Add four tests:
-
no_collision_returns_first_result— closure returnsOk(())immediately. -
one_collision_then_success— counter returns unique-violation once, thenOk. -
retries_exhausted_returns_last_err— counter returns unique-violation 5 times; assertErr(Database(_))withis_unique_violation(). -
non_retriable_error_returns_immediately— closure returnssqlx::Error::RowNotFound; assert counter == 1 (no retries).
-
-
Implement
MockDbErrortest struct implementingsqlx::error::DatabaseErrorwithcode() → "23505"andconstraint() → 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
-
connect_database_invalid_url_returns_err—connect_database("postgres://x:y@127.0.0.1:1/db", 1, Duration::from_millis(100))wrapped intokio::time::timeout(Duration::from_secs(2), …); assertErrcontaining "failed to connect to database". -
init_auth_unreachable_issuer_returns_err—init_auth("http://127.0.0.1:1/realms/none", None); assertErrcontaining "failed to fetch JWKS from Keycloak". -
connect_mq_invalid_url_returns_err—connect_mq("amqp://invalid:1/%2f"); assertErrcontaining "failed to connect to RabbitMQ". -
Strengthen
bootstrap_result_fields_are_public— instantiate aBootstrapResultfrom 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
-
check_docs— factor outsha256_urlanddownload_filebehind a trait orpub(crate) fnthat tests can stub. Three tests drive all threeCheckDocsOutcomevariants:-
all_match_returns_allmatch -
drift_reports_issues_drifted -
missing_reports_issues_missing
-
-
validate::check_preflight— refactor tocheck_preflight_with_probes(skip_docker, nextest_probe, docker_probe)where probes areimpl Fn() → Result<()>. Two tests:-
nextest_missing_returns_err_with_install_hint -
docker_daemon_unreachable_returns_err_with_startup_hint
-
-
devstack— factor dispatch logic into a puredecide_action(force, markers_exist, staleness_result) → StartActionenum (ColdStart,AlreadyCurrent,ContainersDown,CachedRebuild,NoCacheRebuild,VolumeWipe). Unit-test all 6 variants. IO inensure_readystays. -
craig-test-lib::ports_env_overrides— tests withtempfile::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_walking—Cargo.tomlwithout[workspace]isn’t treated as root
Use
serial_testorOnceLockreset pattern for parallel-safety of the cache. -
Verification: cargo nextest run --workspace --lib — ~13 new tests pass across check_docs / validate / devstack / test-lib.
Step 7: E2E — cookie session lifecycle (ADR-013 verification)
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
-
Add
cargo xtask dev restart-service <name>:-
Thin wrapper:
docker::compose(&["restart", &name])?;then a single-service health wait (either updatewait_for_healthto accept a filter or writewait_for_one(svc)) -
Validate
nameagainstcrate::docker::CRAIG_SERVICESto prevent arbitrary docker-compose exec
-
-
Add
tests/e2e/lib/helpers.ts::restartCraigWebAndWait()— shells out tocargo xtask dev restart-service craig-webviachild_process.execFileSyncand returns after the devstack health-check clears. -
Add
auth-cookies.spec.tswith 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— manipulatecraig-sessionvalue viacontext.addCookies, navigate to protected page, expect 302 to Keycloak auth URL. -
expired session cookie redirects to login— override cookie withexpiresin the past, navigate, expect redirect. -
logout clears session cookie— click logout, assertSet-Cookie: craig-session=; Max-Age=0in 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
-
concurrent reports receive distinct confirmation codes(public-report.spec.ts): usePromise.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. -
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. -
partner with no agreements shows empty state(exchange.spec.ts): use the fixture partner created by the existingcreate exchange partnertest (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
-
Reconcile Rules Engine endpoint count: count actual
#[utoipa::path]occurrences inservices/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. -
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. -
Update this plan’s Status table — all steps Complete.
-
Move nav entry from Active → Archive; add row to
archive.adocunder Code Quality.
Verification: cargo xtask check-docs clean. Plan Completion Audit reports zero residual findings.
Files Touched
| File | Change |
|---|---|
|
Add |
|
Remove local const; factor |
|
Remove local const; add |
|
|
|
Factor |
|
Expand sad-path test coverage (Step 5) |
|
Add test module for |
|
Extract |
|
New — cookie lifecycle tests (Step 7) |
|
Add concurrent-submission test (Step 8) |
|
Add empty-dropdown test (Step 8) |
|
Add empty-agreements test (Step 8) |
|
Add |
|
This plan (Step 1) + status updates on completion (Step 9) |
|
Active entry (Step 1) → Archive on completion (Step 9) |
|
Archive row on completion (Step 9) |
|
Endpoint count fix (Step 9) |
|
Document new |
|
Entry under |
Verification
-
cargo nextest run --workspace --lib— ~21 new unit tests pass (4 intake retry + 4 bootstrap + ~13 xtask/test-lib) -
cargo xtask dev reseed -
cargo nextest run --workspace --locked --profile integration— 1182 + new tests pass -
cargo xtask e2e— 145 + 7 new tests pass (4 cookie + 1 concurrent + 1 empty-dropdown + 1 empty-agreements) -
Full
cargo xtask validate+ pre-push gate green -
cargo xtask check-docsclean -
glab issue list --milestone "2026 Q2 — Close Active Plans"shows all 7 previously-unassigned issues attached -
glab mr view 81 | grep labelsandglab mr view 83 | grep labelsshow new labels -
Plan Completion Audit subagent — zero residual findings
Documentation Updates
-
.claude/docs/services.md— endpoint count reconciliation (Step 9) -
.claude/docs/testing.md— document newxtask dev restart-servicesubcommand (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 |
7 |
Plan specified that |