Plan: Build + verify the worker SNAP+TANF+ELE demo workflow (+ applicant closure)

On this page
NOTE

This plan is a living guide, not a frozen spec. The worker workflow has never run end-to-end, so implementation will surface more interconnected, code-dependent issues. That is expected and fine — we work it as a team: when something new appears, resolve it, update this plan’s Design section (ADR-013: plans are living specs), and keep going. The phased structure below is the map; reality during each MR refines it.

Status

MR Description Status

1 (G1)

Intake section forms/BFF emit valid nested payloads (household_composition.members ≥1 from household context; income_employment/resources/expenses_shelter empty verifications); settle the form-encoding. canopy-web + maybe canopy-contracts-applications.

Done (2026-06-01) — hx-target closestfind (the real root cause: htmx:targetError aborted every save before it left the browser) + dead json-enc/JSON-content-type removed + contract serde(default) for the 3 min=0 verifications + BFF synthesizes household_composition.members from canopy-persons membership + intake prefills household_id/head (no hand-typed UUID). Plus a devstack-refresh fix (hash .html templates, not just .rs). 8 unit + 2 live e2e (22/22 green both worker projects).

2 (G2)

NOA generation: add recipient person_id (HoH) to the determination events; route tanf.determined in the notices manifest. canopy-snap/canopy-tanf/canopy-notices.

Done (2026-06-01) — both determination events carry the HoH person_id (new shared canopy_common::household::head_of_household_person_id over the orchestrator members); manifest routes tanf.determined→shared NOA templates (program=tanf). Pure payload builders + 9 unit tests. Live subscriber→NOA assertion deferred to MR10 (pollNotices).

3 (G3)

Verification resolves in the worker flow: on document Accept, resolve the linked verification (or a worker Resolve affordance). canopy-webcanopy-verification.

Done (2026-06-01) — chose auto-resolve-on-Accept (one worker action closes both). canopy-verification GET /v1/verifications gains a document_id filter (EXISTS over verification_responses); canopy-web accept_document queries it post-accept + resolves each linked verification (best-effort, completed_by=worker). Integration test (find-by-doc → resolve → drops). Live applicant→worker closure = MR9.

4 (G5)

Determination records back + parent applications.status recompute → the household leaves the queue. canopy-web/canopy-applications.

Done (2026-06-01) — the orchestrator’s ProgramResult now carries the persisted determination_id + (for denials) denial_reason_codes for verified outcomes; canopy-web’s run_determination + approve_application record each terminal (approved/denied) outcome back via the existing POST …/programs/{program}/determination (best-effort + loud); record_determination recomputes the parent applications.status in the same transaction → all-terminal flips to determined → drops from the submitted|processing queue. pending_verification results excluded (no id, stays gated; the CHECK has no pending_verification). Web unit test (collector) + applications integration test (submitted → processing → determined) + 10 determine snapshots regenerated. Live queue-lifecycle assertion = MR10.

5 (G7)

TANF events carry the canopy-applications application id (cross-service referential integrity for notices/ELE/demo verify). canopy-tanf.

Done (2026-06-01) — tanf.determined + tanf.application_approved now emit ctx.application_id (the orchestrator-supplied canopy-applications id, same as the signed envelope) instead of the unresolvable TANF-local tanf_applications.id; canopy-medicaid’s ELE subscriber consequently records a resolvable ele_grant_events.source_application_id (no medicaid change). Integration test asserts both event payloads carry the canopy id via canopy-security’s audit feed — a true differential (fails pre-fix, passes post-fix). Docs: corrected the stale determination.completed.tanftanf.determined event name + id-contract note.

6 (G8)

Surface the HoH name (discovery marker) on My Queue rows. canopy-web.

Done (2026-06-01) — added an Applicant column (HoH name) to the My Queue dashboard panel + the case-search queue table, resolved through canopy-persons via the same path as the case-detail identity hero, deduped per household + fetched concurrently (futures::join_all); unresolvable households render a muted em-dash; rows carry data-applicant-name for discovery. Template unit tests (name + em-dash) + an e2e structural assertion (column header + per-row marker + em-dash render). NOTE: the real-name-resolves end-to-end assertion is deferred to MR10 — the generated default seed references households that 404 in canopy-persons (0/368 queue apps resolve; the G6 issue MR7 fixes), so no current queue row resolves a name. Resolution correctness is covered transitively — the helper mirrors the proven case-detail identity-hero resolution path (same household→member→person→name lookup; only the miss-fallback differs, empty for the em-dash) + an HTML-escaping unit test.

7 (G9 — discovered)

Orchestrator infers TANF deprivation (deprivation_type/deprivation_verified/dependent_children + HoH applicant_person_id) from household composition so a live TANF determination can approve. canopy-eligibility/canopy-contracts-eligibility.

Done (2026-06-01) — newly-discovered 9th gap (see Design note below): the orchestrator-side ApplicationContext dropped all TANF deprivation fields → a live TANF determination always denied (r-no-deprivation). The orchestrator now infers them from household composition (infer_tanf_deprivation): single caretaker + ≥1 minor child → CSO, verified; dependent-child count from member ages (relationship-label fallback); HoH applicant_person_id forwarded. Fields added to the eligibility ApplicationContext (canopy-tanf already had matching names; others ignore them — no deny_unknown_fields). Provisional, demo-grade inference — flagged in code/docs + tracked to explicit-intake-capture in #669. Unit tests (heuristic + HoH) + a live-determine integration test (single-caretaker-with-child → TANF approves; true differential).

8 (G6 + #654 seed)

Pin the demo reference date + teach the generator to emit the ELE SQL; add the fixed-UUID coherent ELE-determination persona (2-person, dependent child w/ deprivation, zero income, SNAP+TANF, ele_consents input). canopy-seed.

Done (2026-06-01) — generate.rs’s `Utc::now() → pinned DEMO_REFERENCE_DATE (2026-05-28, the date the committed set was generated → regeneration churns nothing else); the generator now emits the ELE consent SQL via render_ele_consents_supplement (relocates the previously hand-edited canopy_medicaid.sql block verbatim + the new persona’s consent), wired in bin/demo.rs after render_medicaid. New coherent persona Amara Okafor (HoH) + child Ada (the distinctive surname is the MR11 queue marker): two-person household, zero income, one submitted application requesting SNAP+TANF (both program rows pending → My-Queue-visible), one ele_consents INPUT row (no grant/ele_status — derived live per ADR-014). The single-caretaker + minor-child shape is exactly MR7/G9’s TANF-deprivation-inference trigger, so TANF approves live. cargo xtask demo regenerate diff = only the 3 intended files (zero churn elsewhere); check-drift ✓; demo verify ✓ for every seed-controlled check (the 3 ele_consents checks now resolve 4 rows each, 0 orphans; the lone audit_events orphan is pre-existing canopy-security runtime data the seed never touches — none reference the new persona). 6 unit tests (date-pin, marker-uniqueness, persona coherence, consent-only, FK-resolvability, UUID-uniqueness).

9 (G4 infra)

--devstack-profile full + profile-aware readiness + full-stack health-wait (no unconditional depends_on). xtask/docker-compose.yml.

Done (2026-06-01) — added a --devstack-profile <snap-only|full> flag to cargo xtask e2e (xtask/src/cmd/e2e.rs), default snap-only (the historical path is byte-identical — a unit test pins the default). For full it brings up every program service additively (docker compose --profile full up -d, after ensure_ready, which cold-starts full but a refresh reuses the stored profile) and then waits for the whole stack to report healthy via the existing wait_for_health — an explicit health-wait rather than a widened canopy-e2e.depends_on (which would force snap-only runs to wait on TANF/Medicaid they never start). The build + run compose invocations swap the hardcoded snap-only for the flag value, and CANOPY_E2E_DEVSTACK_PROFILE is forwarded into the container for MR11’s gate. No docker-compose.yml change needed (the wait lives in Rust; depends_on untouched per the plan). 3 unit tests + a live transition run (--devstack-profile full from a snap-only-marked stack brought TANF/Medicaid/CAPS/WIC back healthy and the targeted spec passed). ADR-005: additive, full-present path.

10

Reusable tests/e2e/lib/ helpers + applicant closure (worker Accept resolves the verification; applicant reflects Accepted).

Done (2026-06-01) — extended the demo-gated applicant-portal.spec.ts walk with the full G3 closure. The applicant now attaches the just-uploaded document when responding to the verification (select[name=document_id]), which links the response to the document; a worker then Accepts the document and the spec asserts, end-to-end across both origins: the worker row reads "Reviewed", the response note drops out of the worker’s PENDING verifications (the attached verification resolved — MR3’s resolve_verifications_for_document), and Maria’s own Documents page shows "Accepted" on the document row (keyed off the row’s status pill / review_status, not the filename). A reusable gotoWorkerCaseSection(workerPage, {householdId, program, section}) helper (the ?focus_section inline-render + domcontentloaded sync pattern, #667) is extracted into tests/e2e/lib/helpers.ts and exercised by every worker step. Verified green: cargo xtask e2e --profile demo --project=applicant-portal → 11 passed (incl. the closure). Deviation (ADR-013): the walk-specific helpers move to MR11 — Playwright helpers can’t be unit-tested, so each lands with the spec that exercises it. MR11 then re-scoped the set to the honest walk: 5 shipped (findInQueueByMarker / runDetermination / pollNotices / pollEleBadge / assertAuditEvent); the planned fillSection / completeDataCollection were dropped as theatre — the worker determines the seeded (applicant-populated) case directly, with no fact-authoring beat (see MR11 + epic &56 / ADR-027).

11

Worker SNAP+TANF+ELE determination walk — the honest demo (queue-discovery by HoH-name marker, two worker contexts, queue lifecycle, exact ELE event chain) on the seeded Okafor case + the 5 walk helpers. The worker reviews the applicant-populated case and determines directly — no fact-authoring theatre. Closes #654.

Done (2026-06-02) — worker-determination-ele.spec.ts + 5 lib/helpers.ts helpers, green via cargo xtask e2e --profile demo --devstack-profile full — --project=worker-determination-ele (9 passed). SNAP+TANF determined live via the shared top-bar Run Determination action; asserts both NOAs (G2), ELE granted→extended (exact chain snap.application_approvedmedicaid.ele.grantedtanf.application_approvedmedicaid.ele.extended, until-date unchanged), and the corrected queue lifecycle (appears → TANF-item-not-lost-after-SNAP → clears only after both programs are terminal, since My Queue filters on application status — the original plan’s "leaves the SNAP queue after SNAP" was wrong for a multi-program app). fillSection/completeDataCollection dropped as theatre (Run Determination has no data-collection gate). Builds on epic &56 / ADR-027.

Context & scope reframe

A coverage audit + four review passes (last three by the user, against the codebase) established that the demo’s worker SNAP+TANF+ELE deep-dive is not fully built — it has broken/incomplete links no E2E exercises, which is why the coverage gaps existed. "Robust coverage" therefore means build the missing links, then prove them with tests — a multi-service build program. The demo itself would break on these gaps when recorded.

User decisions: (1) Full build-out program. (2) Queue/business-label discovery for the worker walk (real My Queue UI; no hardcoded demo UUIDs / Rust-const imports into TS). (3) Iterate this plan as the team’s guide, surfacing more as we go.

G9 (discovered building MR8’s persona): orchestrator drops TANF deprivation

Designing the coherent TANF-approving persona surfaced a 9th gap the original audit missed. For a live TANF determination (worker "Run determination" → orchestrator → canopy-tanf) to approve, canopy-tanf needs deprivation_type + deprivation_verified=true + dependent_children≥1. But the orchestrator-side ApplicationContext (canopy-contracts-eligibility) had no deprivation fields at all, nothing in canopy-applications/intake captures deprivation, and the orchestrator read none — so canopy-tanf received None/None and always denied (r-no-deprivation / r-dep-not-verified). No seed data can fix this (the persona’s deprivation never reaches canopy-tanf). It is a hard prerequisite for the MR11 walk’s "TANF approves" beat + the ELE granted→extended chain (which needs tanf.application_approved).

Decision (user, 2026-06-01): the orchestrator infers deprivation from household composition as a provisional, demo-grade simplification — a single caretaker with ≥1 minor child → continued-absence (CSO), verified; dependent-child count from member ages. This is deliberately not policy-correct (real deprivation is multi-factor + worker-verified per 45 CFR 261 / PAMMS 1510-1515); it is flagged in code + docs and tracked to explicit intake capture in #669. Built as MR7 (plumbing) ahead of MR8’s persona (overlay), keeping the two concerns separate.

This is Plan 4 of the demo-video split: Plan 1 (worker intake + program independence) and Plan 2 (ELE 1-year flag) are archived; Plan 3 (applicant portal, Dioxus) is complete. Plan 4 closes the gaps that keep the worker SNAP+TANF+ELE narrative + the applicant→worker closure from actually running end-to-end.

Verified build gaps (each: file:line + decided fix)

G1 — section forms/BFF emit invalid payloads. HouseholdCompositionPayload.members is #[validate(length(min=1))] (crates/canopy-contracts-applications/src/sections.rs:211); income_employment/resources/expenses_shelter verifications are min=0 but the field must be present (L284/317/361). The minimum-viable forms (services/canopy-web/templates/applications/_intake_section_form.html) collect none; the BFF flat→JSON converter (services/canopy-web/src/api/applications.rs ~L1462) doesn’t synthesize them → PUT 422. hx-ext="json-enc" is unbacked (the BFF parses form-encoding). Fix: pass household-membership context into the intake render/submit (a pure converter can’t derive members) so household_composition emits ≥1 members; the BFF emits verifications: [] for the 3 min=0 sections (or add #[serde(default)] to the contract); settle the encoding explicitly. Contract round-trip + live UI-save tests.

NOTE
G1 design refined during MR1 implementation (2026-06-01)

The live section-save was broken by four stacked defects — and the load-bearing one is not the encoding. Running the real htmx path in an E2E (which cargo xtask validate does not do — it stops at nextest + doctest + docker build) was required to find it; the unit tests + validate were all green while the browser save was still 100% broken.

  1. hx-target (the actual root cause). The form’s hx-target="closest .intake-section__form-status" points at a <div> that is a descendant of the form, but closest walks ancestors — so htmx raised htmx:targetError and aborted the request before it left the browser. No request ever reached canopy-web (confirmed via a page.on('console') probe: htmx:targetError, zero /sections/ requests, zero handler logs). Fixed to find .intake-section__form-status (htmx’s first-matching-descendant). This is why no worker has ever saved a section from the UI — the plan’s original "415 at the Form extractor" was a correct-but-secondary diagnosis (the request never got far enough to 415).

  2. Encoding (secondary). hx-ext="json-enc" is not loaded anywhere, and the form also set hx-headers='{… "Content-Type":"application/json"}'; once the request does fire, that urlencoded-body-under-JSON-content-type would 415 at axum::extract::Form. Both removed so htmx posts clean application/x-www-form-urlencoded; the BFF stays the JSON-shaping authority.

  3. household_composition.members — synthesized server-side in the proxy from canopy-persons membership (person_id + relationshiprelationship_to_head); the render prefills household_id (readonly) + head (editable) so no UUID is hand-typed (the MR10 discovery principle). Head-only fallback keeps min=1 when membership is unreachable.

  4. verifications (3 min=0 sections)#[serde(default)] on the contract (a missing list → [] for every client), not a BFF patch.

Infra fix folded in (fix-as-encountered): the devstack refresh (xtask/src/devstack_guard.rs) hashed only .rs for its source-staleness check, so the .html template edit never triggered a canopy-web rebuild — cargo xtask e2e kept running the old binary and the income-save test failed identically twice until the binary was actually rebuilt. The source hash now covers .rs and .html (Askama compiles templates into the binary). Without this, every future template edit in this plan would silently test stale code.

Live proof: 22/22 intake-partial-demo pass in both snap-worker + tanf-worker (income_employment saves through the real htmx form → success banner; household_composition render-prefill is a UUID). The existing 5 specs still pass (the income save targets a section no other spec asserts seeded-state on).

Observed, deferred to MR7 (seed coherence, not MR1): in the generated default e2e seed, some applications reference a household_id that 404s in canopy-persons (canopy_web::api::applications "failed to fetch household"), so the head prefill renders empty there. MR1’s code handles it gracefully (readonly household_id from the app still shows; head-only/empty fallback). The coherent 2-person ELE persona MR7 builds must have a real, resolvable household so household_composition synthesizes a non-empty members. Tracks with G6/G7.

G2 — NOA won’t generate. determination.completed.snap lacks person_id (services/canopy-snap/src/events.rs:48-57); the notices subscriber reads person_id from the event and early-returns if missing (services/canopy-notices/src/main.rs:154-157); the manifest routes SNAP but TANF emits tanf.determined, not determination.completed.tanf (manifest.toml:261). canopy-notices has no persons client (config.rs:18). Fix (decided): add the recipient person_id (HoH) to the determination events in canopy-snap/tanf (leaner than wiring a persons client into notices); route tanf.determined in the notices manifest. Subscriber test for both programs.

G3 — verification never resolves in the worker flow. POST /v1/verifications/{id}/resolve exists (services/canopy-verification/src/api/verifications.rs:114) but canopy-web doc-accept (services/canopy-web/src/api/actions.rs:349) never calls it. Fix: on document Accept, resolve the verification(s) whose response attached that document_id (find via canopy-verification responses) — or add an explicit worker Resolve affordance (decide UX in impl). Wire + test.

G4 — full-profile e2e is not guaranteed. services/…​/xtask/src/cmd/e2e.rs hardcodes --profile snap-only (L191-202, L206); ensure_ready/auto_refresh reuse the stored profile (xtask/src/devstack_guard.rs:751); canopy-e2e.depends_on is web+portal only (docker-compose.yml:1220). Fix: a --devstack-profile <snap-only|full> flag that makes readiness profile-aware (bring up the requested profile’s services), swaps the e2e compose profile, and does NOT add unconditional depends_on on full-profile services (that would break snap-only runs) — instead an explicit full-stack health-wait for the full run; forward CANOPY_E2E_DEVSTACK_PROFILE=full for MR10’s gate.

G5 — determination result is never recorded; the queue never clears. run_determination (services/canopy-web/src/api/applications.rs:866) calls eligibility + re-renders the tab but never persists the result; the store status update doesn’t recompute the parent application status (store/mod.rs:510). So after Complete Data Collection + determination the application stays processing forever and the household never leaves the queue — the core worker beat doesn’t complete. Fix: wire run_determination to record the determination into canopy-applications and recompute the parent applications.status from the program statuses so a terminal decision clears the queue. Tests assert the status transition + queue removal.

G6 — the seed isn’t regenerable. tools/canopy-seed/src/demo/generate.rs:87 uses Utc::now().date_naive() (non-deterministic), and the committed devstack/demo-dataset/canopy_medicaid.sql ELE block is hand-edited (the generator doesn’t emit ele_* SQL — sql_extras.rs:234), so cargo xtask demo regenerate would churn dates and wipe the ELE block. Fix (prerequisite for MR7): pin a fixed demo reference-date constant in the generator; teach the generator to emit the ELE consent + TRUNCATE SQL so regenerate reproduces it and cargo xtask demo check-drift passes.

G7 — TANF crosses service boundaries with the wrong id. TANF events carry the TANF-local application_id (services/canopy-tanf/src/api/handlers.rs:118, events.rs:37); medicaid stores it as source_application_id (main.rs:508); but cargo xtask demo verify + ELE expect canopy_applications.applications.id (xtask/src/cmd/demo.rs:319). Fix: TANF events must carry the canopy-applications application id (normalize, or add a distinct canopy_application_id field) so notices/ELE/demo-verify referential integrity holds — without breaking program isolation (ADR-001/002).

G8 — My Queue exposes no discovery marker. Queue items carry no applicant/HoH name/marker (services/canopy-web/src/…​/my_queue.rs:21); templates render case/type/program/status/due/action only (search.html:18). Queue-discovery (the chosen test approach) can’t find the persona. Fix: surface the HoH name (or a household label) on My Queue rows (row data + template) so the walk discovers the case by a stable business marker — also a genuine worker-UX improvement.

Phased MRs (build → seed/infra → test)

Order within a phase is flexible; phases are sequential (tests need the build + seed + infra).

  • MR1 (G1): intake section forms/BFF nested payloads + household context. canopy-web (applications.rs, _intake_section_form.html), maybe canopy-contracts-applications. Labels: type::feature, priority::high, service::web, program::cross-program, workflow::in-progress.

  • MR2 (G2): person_id on determination events + tanf.determined notices routing. canopy-snap, canopy-tanf, canopy-notices. Labels: type::bug, priority::high, service::notices, program::snap, program::tanf.

  • MR3 (G3): verification-resolve on doc-accept. canopy-web/api/actions.rs + Verifications section → canopy-verification. Labels: type::feature, priority::high, service::web, service::verification, program::cross-program.

  • MR4 (G5): determination records back + parent application-status recompute → queue clears. canopy-web, canopy-applications. Labels: type::bug, priority::critical, service::web, service::applications, program::cross-program.

  • MR5 (G7): TANF events carry the canopy-applications application id. canopy-tanf. Labels: type::bug, priority::high, service::tanf, program::tanf, compliance::irs-pub-1075-audit.

  • MR6 (G8): HoH-name marker on My Queue rows. canopy-web. Labels: type::feature, priority::medium, service::web, program::cross-program, compliance::wcag-21-aa.

Phase 2 — seed + infra

  • MR7 (G6 + #654 seed): pin the demo reference date + teach the generator to emit ELE SQL; add the fixed-UUID persona (Rust render_*_supplement in sql_extras.rs + bin/demo.rs, then cargo xtask demo regenerate + commit + check-drift ✓): a 2-person household (adult HoH + a dependent child with a deprivation basis so TANF approves — e.g. absent-parent), no income rows, a SNAP+TANF application submitted/programs pending, no pre-completed sections, a distinctive HoH last-name marker for MR10 discovery (distinct from the existing phase9 app). The generator-emitted demo canopy_medicaid.sql TRUNCATEs ele_consents + ele_status + ele_grant_events and seeds one ele_consents row (consent INPUT only — chain state is derived live per ADR-014) using the persona’s fixed UUIDs (so demo verify’s orphan checks pass); the TRUNCATE gives deterministic ELE-clean loads so MR10’s granted-then-extended is reproducible. Labels: `type::feature, priority::high, service::seed, program::cross-program. Docs: add identifiers to runbooks/demo-applicant-credentials.adoc.

  • MR8 (G4 infra): --devstack-profile full + profile-aware readiness + a full-stack health-wait (NOT unconditional depends_on). xtask/cmd/e2e.rs, devstack_guard.rs, docker-compose.yml. Labels: type::chore, priority::high, service::ci, service::xtask, program::infrastructure. ADR-005: additive, demo-gated, full-present path.

Phase 3 — tests (the proof)

  • MR9 (helpers + applicant closure): reusable tests/e2e/lib/ helpers — fillSection, completeDataCollection, runDetermination, pollNotices, pollEleBadge, assertAuditEvent, findInQueueByMarker — with 30–60 s polling + diagnostics. Extend applicant-portal.spec.ts: applicant attaches the uploaded doc to the verification → worker Accepts → assert doc "Accepted" + (G3) the verification resolves; the applicant side reflects "Accepted" keyed off the document row/metadata, not the filename. Labels: type::feature, priority::high, service::ci, service::web, service::portal, program::cross-program.

  • MR10 (worker SNAP+TANF+ELE walk): a new demo-gated tests/e2e/specs/worker-determination-ele.spec.ts + project gated on CANOPY_E2E_SEED_PROFILE==='demo' AND CANOPY_E2E_DEVSTACK_PROFILE==='full', added to the default caseworker testMatch exclusion (playwright.config.ts:46). Two browser contexts (auth/snap-worker.json, auth/tanf-worker.json) — one project can’t hold both. The walk:

    1. SNAP worker → My Queue → find the household by the MR6 HoH-name marker (asserts queue visibility; no hardcoded UUID) → Run Determination via the shared top-bar action → assert .u-hero-status "APPROVED" + .u-hero-amount /\$[\d,]/+. (Honest: the facts are the applicant-submitted/seeded canopy-persons data — the worker reviews and determines; there is NO fillSection/Complete-Data-Collection beat, because that intake JSON the determination never reads is exactly the theatre epic &56 / ADR-027 exists to replace. Run Determination has no data-collection gate.)

    2. pollNotices → SNAP NOA (G2); pollEleBadge → "ELE active for 1 child until {date}", capture the date.

    3. TANF worker (a second context) → My Queue still shows the same household’s TANF item (NOT lost by the SNAP determination — a multi-program app leaves the queue only once every program is terminal, since My Queue filters on application status; the original "leaves the SNAP queue after SNAP" was wrong) → Run Determination → APPROVED (via the seeded single-caretaker + minor-child deprivation, orchestrator-inferred per #669, provisional) → TANF NOA (G2) → now both programs terminal → the app is determined and clears the queue for both workers.

    4. ELE: assertAuditEvent for the exact names — snap.application_approvedmedicaid.ele.grantedtanf.application_approvedmedicaid.ele.extended (the audit section is per-household with no program column — assert the event_types appear; the per-program-chain separation is a backend/ADR-014 property, not UI-asserted) — deterministic because MR7 loads ELE-clean; assert the until-date unchanged and the extended event fired (the date alone is insufficient).

      Labels: type::feature, priority::high, service::web, service::ci, program::cross-program, compliance::wcag-21-aa, compliance::irs-pub-1075-audit. Docs: flip the ele-badge.spec.ts #654 deferral comment; update the roadmap.adoc Remaining Work Tracker; note the walk in the Antora e2e page.

Test-design principles (Phase 3)

Queue/business-marker discovery (no UUID/Rust-const imports into TS); two browser contexts for the two worker roles; exclude the new spec from the default project; exact event-name assertions; 30–60 s shared polling helpers with diagnostics (RabbitMQ/outbox/subscriber on a cold full stack); decouple from the exact demo filename; assert queue lifecycle (appears, not-lost-across-programs, clears) — not just NOAs/events.

Shared conventions (every MR)

Branch from synced main; targeted git add; a fresh D1–D8 subagent on the staged diff reported inline; a two-stage PRECOMMIT_TOKEN=<token> git commit (never --no-verify); the commit trailer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>; cargo fmt --all; a pre-push cargo xtask validate; glab mr create; force-merge = cancel-MWPS POST (406 ok) + PUT merge squash=false&should_remove_source_branch=true (workflow-conventions.md overrides the Tier-1 "never force-merge"); sync main + update memory after each merge; Closes #654 on MR7/MR10. Labels per-MR (the type:: closed set has no test → use feature/bug/chore). Canonical docs land in Antora (CLAUDE.md doc-homes); a CHANGELOG bullet every MR. Keep MRs ≤500 LOC (split where the regenerated-SQL diff or a multi-service change pushes over).

Issues to file (ADR-013 — by number, not buried in this plan)

  • type::chore: assertion-hardening of the "response < 500" worker-action specs (actions.spec.ts, worker-portal-*-actions.spec.ts, renewals.spec.ts).

  • type::feature (optional): a worker ELE-consent capture affordance (none exists today; the demo consent is seeded).

  • #654 is closed by MR10; #667 (tabs-shell get_tab composition gap) stays related (the walk uses ?focus_section=).

Verification (per MR)

  • MR1: contract round-trip + a live PUT …/sections/snap/household_composition returns 200, not 422.

  • MR2: a notices subscriber test emits a SNAP and a TANF NOA with a resolved recipient.

  • MR3: accepting a doc resolves the linked verification (gone from ?status=pending).

  • MR4: after determination the application status transitions terminal + the household leaves the queue.

  • MR5: a TANF event’s application_id resolves to canopy_applications.applications.id; demo verify clean.

  • MR6: My Queue renders the HoH-name marker.

  • MR7: demo regenerate is byte-stable (pinned date) + check-drift ✓; demo verify ✓; --profile demo --reset loads clean twice; a seed test pins SNAP-approves + TANF-approves + ELE-gate-passes + ELE-clean load.

  • MR8: cargo xtask e2e --devstack-profile full brings the full stack healthy from a snap-only starting stack; snap-only e2e still works.

  • MR9: cargo xtask e2e --profile demo — --project=applicant-portal — Accept resolves the verification; the applicant reflects Accepted (row-keyed).

  • MR10: cargo xtask e2e --profile demo --devstack-profile full — --project=worker-determination-ele green — queue-discovered SNAP+TANF determinations APPROVE, the queue clears, NOAs generate, ELE granted-then-extended (exact events). A deliberately-wrong assertion fails (sanity); the ele-badge.spec.ts absent-case guard still passes.

  • Every MR: a D1–D8 staged-diff review; a pre-push cargo xtask validate.

Risks / verify-during-impl (expect more — we handle as a team)

  • G1 members source (household context into render/submit); serde-default vs explicit empty arrays.

  • G2 HoH person_id availability in the determination handler at publish time.

  • G3 doc↔verification link + the resolve UX.

  • G4 profile-aware readiness without disrupting a running stack; no unconditional full depends_on.

  • G5 the right place to record + recompute status (web vs applications) + queue-removal semantics.

  • G6 pinned-date scope (does it churn other archetypes?) + the generator’s ELE-SQL emission.

  • G7 cross-service ID normalization without breaking program isolation (ADR-001/002).

  • TANF approval needs real deprivation/dependent-child context flowing from the filled sections.

  • ELE idempotency via the demo-SQL TRUNCATE of the three ELE tables (the chain tables are excluded from --reset per ADR-014; the demo SQL’s own TRUNCATE is the reset mechanism).

  • full e2e cost/time; keep it demo-gated + out of the default validate e2e.

  • More will surface — that is expected; resolve, update this plan’s Design section, continue.

Edit this page · default