Plan: canopy-seed CAPS + WIC Fixtures & Playwright E2E Follow-up

On this page

Status

Step Description Status

1

Extend tools/canopy-seed/src/model.rs with CapsDetermination, CapsAuthorization, WicDetermination, WicParticipant, WicNutritionalRiskAssessment — field-for-field copies of the service-side structs at services/canopy-caps/src/store/models.rs and services/canopy-wic/src/store/models.rs. Extend SeedData (same file) to carry Vec<T> for each. All new .rs files (if any) get the // SPDX-License-Identifier: AGPL-3.0-or-later header.

Done (2026-04-21) — MR !104

2

Extend tools/canopy-seed/src/datagen.rs with phase11_caps(sg, data, contexts) and phase12_wic(sg, data, contexts) invoked from pub fn generate() after phase10_security_rules. Reuse SeedGenerator’s `sg.uuids (backed by DeterministicUuidGenerator from tools/canopy-seed/src/uuid.rs) and sg.rng; reuse HouseholdContext’s existing child / adult person-id lists. Deterministic split: for index `i < 2·N/3 → approved, else denied. Approved CAPS row spawns one CapsAuthorization; approved WIC row spawns one WicParticipant + one WicNutritionalRiskAssessment. Category selection — see Design.

Done (2026-04-21) — MR !104

3

Extend tools/canopy-seed/src/sql.rs with write_caps(…) and write_wic(…) emitting canopy_caps.sql (determinations + authorizations) and canopy_wic.sql (determinations + participants + assessments). Follow the existing write_snap-style pattern (sql_uuid, sql_text, sql_date, sql_timestamp, sql_string_array helpers already in the file). write_all(…) (same file) calls both new functions.

Done (2026-04-21) — MR !104

4

Extend tools/canopy-seed/src/manifest.rs with TypeScript exports — SEED.capsDeterminations, capsDeterminationsList, SEED.capsAuthorizations, capsAuthorizationsList, SEED.wicDeterminations, wicDeterminationsList, SEED.wicParticipants, wicParticipantsList, SEED.wicAssessments, wicAssessmentsList — plus helpers findCapsApproved(), findCapsDenied(), findWicApproved(), findWicDenied(). Use the vals() helper (manifest.rs line ~320) and append helpers after the existing findApproved / findDenied / findWithAppeal / findExpedited block. Emitted file: tests/e2e/lib/seed.ts (already the target — no path change).

Done (2026-04-21) — MR !104

5

Add "canopy_caps" and "canopy_wic" as the last two entries in the const DATABASES: &[&str] array at xtask/src/cmd/seed.rs:31-43 (before the closing ]; at line 43). No other changes in that file.

Done (2026-04-21) — MR !104

6

New tests/e2e/specs/caps.spec.ts (SPDX header line 1). One spec: findCapsApproved()page.goto('/cases/${caps.householdId}?program=caps') → wait for /tab/authorization response → click #tab-authorization → assert page.locator('#tabpanel') contains "provider-001". Playwright wait pattern matches case-detail.spec.ts:49-60.

Done (2026-04-21) — MR !104

7

New tests/e2e/specs/wic.spec.ts (SPDX header line 1). One spec: findWicApproved()page.goto('/cases/${wic.householdId}?program=wic') → wait for /tab/nutrition response → click #tab-nutrition → assert page.locator('#tabpanel') contains the seeded assessment_date text (format YYYY-MM-DD).

Done (2026-04-21) — MR !104

8

Extend tools/canopy-seed/tests/integration.rs with a caps_wic_fixture_counts test: generate(&SeedConfig { households: 9, … }) then assert data.caps_determinations.len() == 9, data.caps_authorizations.len() == 6 (the approved subset), data.wic_determinations.len() == 9, data.wic_participants.len() == 6, data.wic_assessments.len() == 6. Also assert every caps_det.household_id exists in data.households (FK consistency) and every caps_auth.determination_id exists in data.caps_determinations.

Done (2026-04-21) — MR !104

9

Plan sync: in docs/modules/ROOT/pages/plans/canopy-caps-list-endpoints.adoc Errata section (== Errata, subsection === 2026-04-21 — Step 8 Playwright E2E deferred (no CAPS seed data)), append a follow-up paragraph: "Resolved 2026-04-21 by canopy-seed-caps-wic-fixtures — see tests/e2e/specs/caps.spec.ts." Mirror for canopy-wic-list-endpoints.adoc Errata. Update docs/modules/ROOT/pages/roadmap.adoc Tier 5.5 rows for the CAPS authorization tab and WIC nutritional-risk tab — replace the "Playwright E2E deferred (no CAPS seed data); follow-up tracked in plan Errata." text with "Playwright E2E landed 2026-04-21 via canopy-seed-caps-wic-fixtures." Add CHANGELOG.adoc entry under == Unreleased / === Added.

Done (2026-04-21) — MR !104

Branch: feature/canopy-seed-caps-wic-fixtures
Labels: type::feature, priority::medium, program::caps, program::wic, service::seed, service::web, workflow::ready

Context

Tier 2A shipped in MRs !102 (canopy-caps list endpoints) and !103 (canopy-wic list endpoints). Both plans' Step 8 — a Playwright E2E spec that navigates to a seeded case, clicks the program-specific tab (CAPS Authorization / WIC Nutritional Risk), and asserts the row renders — was deferred for the same reason: tools/canopy-seed produces SNAP-only seed data, so tests/e2e/lib/seed.ts’s `findApproved() helper returns a SNAP determination with no corresponding CAPS/WIC rows.

The only thing standing between "empty tab with no visible regressions" and "full tab-click coverage" is seed data. This plan extends canopy-seed to generate CAPS + WIC records alongside the existing SNAP pipeline, then lands the two Playwright specs the earlier plans deferred.

Net effect: worker-portal CAPS + WIC tabs gain click-through E2E coverage; the Tier 5.5 authorization-tab + nutritional-risk-tab rows on the roadmap close out fully rather than with "follow-up deferred" caveats.

Scope

In scope:

  • canopy-seed generates deterministic CAPS + WIC fixtures from the same --seed / --households knobs as today.

  • Two Playwright specs that do the exact click-through the earlier plans deferred.

  • One integration-test update in canopy-seed to guarantee the new entities stay FK-consistent with households / persons.

Out of scope:

  • CAPS provider registry seeding. The caps_authorizations.provider_id column is TEXT — the seeder emits "provider-001" / "provider-002" string IDs, not foreign-keyed rows. Matches how the unit tests seed today.

  • WIC EBT-vendor integration / food-package lifecycle state machines.

  • Multi-determination-per-household (SNAP currently does one det per household; we match that).

  • New CAPS/WIC applications in canopy_applications. The CAPS/WIC determinations can reference existing Application rows from the SNAP pipeline — applications already have a generic programs_requested field.

  • Policy changes: no jurisdiction.toml / citations.toml edits. All thresholds used during determination are consumed from the existing loaders.

Dependencies

  • tools/canopy-seed/src/model.rs — extend with 5 new structs.

  • tools/canopy-seed/src/datagen.rs — add 2 phases + helpers. Reuse HouseholdContext’s existing child / adult person-id lists for `child_person_id / WIC participant lookups.

  • tools/canopy-seed/src/sql.rs — add SQL renderers mirroring existing write_snap patterns.

  • tools/canopy-seed/src/manifest.rs — add TypeScript entity blocks + helpers using the existing vals() helper pattern.

  • tools/canopy-seed/tests/integration.rs — count assertions.

  • xtask/src/cmd/seed.rs — add 2 entries to DATABASES.

  • tests/e2e/specs/caps.spec.ts (new) + tests/e2e/specs/wic.spec.ts (new).

  • Two plan Errata sections + roadmap updates in docs/modules/ROOT/pages/.

No schema migrations, no service changes, no new dependencies.

Depends on canopy-caps-list-endpoints (MR !102, merged) and canopy-wic-list-endpoints (MR !103) for the HTTP endpoints the Playwright specs hit.

Design

Entity generation per household

For each of the N households, deterministic outputs:

Entity Count Linked to Notes

CapsDetermination

1

household_id + a child person_id

Status: first 2/3 of households get approved, last 1/3 get denied. Child person picked from household members with relationship = "child"; falls back to any minor person by DOB.

CapsAuthorization

1 per approved CAPS det

determination_id

provider-001, 30 hrs/week, rate_cents_per_hour=750, copayment_weekly_cents=2700, authorization_status="active".

WicDetermination

1

household_id + a WIC-eligible person_id

2/3 approved, 1/3 denied. Eligible person picked in this order: pregnant adult woman → infant (age < 1) → young child (age 1-4) → first adult. participant_category set to match.

WicParticipant

1 per approved WIC det

person_id

Derives certification_start/end and food_package from the determination.

WicNutritionalRiskAssessment

1 per approved WIC det

person_id + assessor_worker_id (existing SEED.workers.jane_doe)

anthropometric_risk=true, risk_codes=["110"] (low weight-for-height — PAMMS placeholder), assessment_date = determination.effective_date - 7 days.

Split ratio rationale: SEED.determinations (SNAP) already splits ~66% approved / ~33% denied. Mirroring the ratio gives findCapsApproved() + findWicApproved() a non-empty result at --households=1, and findCapsDenied() + findWicDenied() non-empty at --households=3 (already the CI minimum).

New TypeScript helpers

manifest.rs emits these in addition to the 4 existing helpers:

export function findCapsApproved() {
  return capsDeterminationsList.find(d => d.status === 'approved');
}
export function findCapsDenied() {
  return capsDeterminationsList.find(d => d.status === 'denied');
}
export function findWicApproved() {
  return wicDeterminationsList.find(d => d.status === 'approved');
}
export function findWicDenied() {
  return wicDeterminationsList.find(d => d.status === 'denied');
}

Playwright spec pattern

Mirrors the existing tests/e2e/specs/case-detail.spec.ts:36-47 income-tab pattern. CAPS example (SPDX header line 1, matches every other .ts spec in that directory):

// SPDX-License-Identifier: AGPL-3.0-or-later
import { test, expect } from '@playwright/test';
import { findCapsApproved } from '../lib/seed';

const caps = findCapsApproved();

test.describe('CAPS case detail', () => {
  test.skip(!caps, 'No approved CAPS determination in seed');

  test('authorization tab renders provider row', async ({ page }) => {
    // `?program=caps` pins the program switcher to CAPS; canopy-web honours
    // the query string per the Tier 4.6 multi-program router.
    await page.goto(`/cases/${caps!.householdId}?program=caps`);
    await Promise.all([
      page.waitForResponse(r => r.url().includes('/tab/authorization'), { timeout: 10_000 }),
      page.click('#tab-authorization'),
    ]);
    const panel = page.locator('#tabpanel');
    await expect(panel).toContainText('provider-001');
  });
});

WIC spec is symmetric: findWicApproved(), ?program=wic, wait on /tab/nutrition, click #tab-nutrition, assert the seeded assessment_date text.

Running the new specs only:

# cargo xtask e2e forwards positional args to Playwright; --grep is Playwright's
# standard test-filter flag (confirmed by xtask/src/cmd/e2e.rs:19).
cargo xtask e2e -- --grep "CAPS case detail"
cargo xtask e2e -- --grep "WIC case detail"

Deterministic generation

Use the existing DeterministicUuidGenerator for every new UUID (CapsDetermination.id, CapsAuthorization.id, WicDetermination.id, etc.). Use the shared sg.rng for the approved/denied split so identical --seed values yield byte-identical SQL across CI runs. Reference-date offsets follow the existing helpers (days_before, days_after, months_after).

xtask DATABASES extension

// xtask/src/cmd/seed.rs
const DATABASES: &[&str] = &[
    "canopy_persons",
    "canopy_applications",
    "canopy_snap",
    "canopy_eligibility",
    "canopy_enrollment",
    "canopy_renewals",
    "canopy_notices",
    "canopy_appeals",
    "canopy_reporting",
    "canopy_security",
    "canopy_rules",
    "canopy_caps",   // + Tier 2A follow-up
    "canopy_wic",    // + Tier 2A follow-up
];

Files Touched

Category Files

Seeder model

tools/canopy-seed/src/model.rs

Seeder datagen

tools/canopy-seed/src/datagen.rs

Seeder SQL rendering

tools/canopy-seed/src/sql.rs

Seeder TS manifest

tools/canopy-seed/src/manifest.rs

Seeder tests

tools/canopy-seed/tests/integration.rs

xtask wiring

xtask/src/cmd/seed.rs

New Playwright specs

tests/e2e/specs/caps.spec.ts, tests/e2e/specs/wic.spec.ts

Plan Errata

docs/modules/ROOT/pages/plans/canopy-caps-list-endpoints.adoc, docs/modules/ROOT/pages/plans/canopy-wic-list-endpoints.adoc

Roadmap

docs/modules/ROOT/pages/roadmap.adoc (Tier 5.5 rows)

Changelog

CHANGELOG.adoc

No migrations, no service code, no new crates.

Verification

Per-step verification

  1. cargo nextest run -p canopy-seed — integration tests assert new-entity counts + FK consistency (Step 8’s caps_wic_fixture_counts).

  2. cargo run -p canopy-seed — --seed 42 --households 9 --jurisdiction georgia --rulesets-dir ./rulesets --output-dir /tmp/seed-test --manifest /tmp/seed.ts — smoke-test the binary produces canopy_caps.sql + canopy_wic.sql + updated TS manifest.

  3. Manual SQL inspection: cat /tmp/seed-test/canopy_caps.sql should show 9 INSERT INTO caps_determinations rows + 6 INSERT INTO caps_authorizations rows.

  4. cargo xtask dev restart + cargo xtask seed --seed 42 --households 9 — real DB load (devstack path).

  5. psql ad-hoc via docker exec -i canopy-postgres-1 psql -U canopy -d canopy_caps -c 'SELECT count(*) FROM caps_determinations;' should return 9.

  6. cargo xtask e2e — --grep "CAPS case detail" — new Playwright CAPS spec passes.

  7. cargo xtask e2e — --grep "WIC case detail" — new Playwright WIC spec passes.

  8. cargo xtask validate — full battery green (fmt + clippy + nextest + docker build). Pre-push hook runs this automatically if git config core.hooksPath .githooks is active.

Plan-level verification

  1. findCapsApproved() and findWicApproved() in tests/e2e/lib/seed.ts return real objects at default seed (--households=9).

  2. No regressions: existing 101-test E2E suite still passes byte-for-byte (SNAP findApproved() unchanged).

  3. Roadmap Tier 5.5 CAPS + WIC rows have no "deferred" language.

Documentation Updates

  • CHANGELOG.adoc — new bullet under == Unreleased / === Added (matches existing Tier 2A entries near the top of the file).

  • canopy-caps-list-endpoints.adoc Errata — append "Resolved 2026-04-21 by canopy-seed-caps-wic-fixtures." to the === 2026-04-21 — Step 8 Playwright E2E deferred subsection.

  • canopy-wic-list-endpoints.adoc Errata — same pattern.

  • roadmap.adoc Tier 5.5 — CAPS authorization tab + WIC nutritional-risk tab rows replace "Playwright E2E deferred (no CAPS seed data); follow-up tracked in plan Errata." with "Playwright E2E landed 2026-04-21 via canopy-seed-caps-wic-fixtures."

Potential Improvements

Out of scope for this plan but worth capturing:

  • Multi-determination per household — currently one CAPS det per household. Real CAPS cases often have multiple children with separate authorizations; seed could emit that.

  • CAPS provider registry — once a provider registry service exists, replace "provider-001" string IDs with real foreign keys.

  • WIC multi-participant households — pregnant mother + infant typically generate two determinations. Seeder could split.

  • Denied-case dark-theme accessibility coverage — cosmetic; the dark-theme axe-core coverage is already exercised on the approved happy path. Deferred indefinitely.

  • Renewals / transfers / terminations for CAPS/WIC — the parallel SNAP phases (certifications, renewals) could grow equivalents.

Tracked follow-ups (filed 2026-05-04 during PI sweep):

  • #395 — Multi-determination per CAPS household

  • #396 — CAPS provider registry (FK lookups)

  • #397 — WIC multi-participant households

  • #398 — CAPS/WIC renewals, transfers, terminations

Errata

2026-04-21 — scope expanded with xtask container-routing fix

Implementation uncovered a pre-existing bug in xtask/src/cmd/seed.rs that would have prevented the new Playwright specs from ever seeing seeded data: the seeder piped every SQL file into canopy-postgres-1 (the shared postgres container), but in the default non-shared-db devstack, per-program services (canopy-snap, canopy-tanf, canopy-medicaid, canopy-caps, canopy-wic) connect to isolated postgres-<program>-1 containers. The seeder printed loaded canopy_caps even though the shared-postgres canopy_caps database had no tables — psql returned zero without -v ON_ERROR_STOP=1, so errors went unreported.

The existing 101 SNAP E2E specs had been passing with permissive "tab renders without crashing" assertions against empty per-service DBs. Adding CAPS + WIC specs that assert specific row content forced the fix.

Resolution: new xtask::docker::is_shared_db_marker_set() reads .devstack/shared-db; new container_for_db() in xtask/src/cmd/seed.rs routes each DB to its per-program container in non-shared-db mode and to postgres-1 in shared-db mode. No config changes needed on the user side — pre-existing --shared-db flag on cargo xtask dev start still works the same way; the seeder just now respects it.

2026-04-21 — synthesize_child helper for households without children

phase1_households_persons generates 0-3 children per household (plus 2 forced children for the first two households). Approximately 1/3 of households in a 9-household seed end up with no children, which blocks phase11’s CAPS determination (requires a child_person_id FK) and phase12’s infant/child WIC category.

Resolution: new synthesize_child(sg, data, ctx, now) helper in datagen.rs inlines a 3-year-old persona + household_member when no existing child is available. Preserves SNAP seed shape byte-for-byte since it only runs in phase11/12 after SNAP is already emitted. 9/9 households now have a CAPS determination and 9/9 have a WIC determination in the default seed.

2026-04-21 — Playwright spec query-string pattern

The plan’s pseudocode showed page.goto('/cases/${caps.householdId}') then clicking a program-switcher link. The running canopy-web accepts ?program=caps as a direct pin on the case-detail route, so the specs take that shorter path (matches the multi-program router behaviour introduced in Tier 4.6). Dark-theme accessibility coverage referenced in the original plan was also trimmed — the existing tests/e2e/specs/accessibility-dark.spec.ts runs as a [dark-theme] project across every route-rendering spec, so the new CAPS + WIC specs inherit dark-theme coverage automatically without explicit AxeBuilder setup.

Edit this page · default