Plan: canopy-seed CAPS + WIC Fixtures & Playwright E2E Follow-up
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Extend |
Done (2026-04-21) — MR !104 |
2 |
Extend |
Done (2026-04-21) — MR !104 |
3 |
Extend |
Done (2026-04-21) — MR !104 |
4 |
Extend |
Done (2026-04-21) — MR !104 |
5 |
Add |
Done (2026-04-21) — MR !104 |
6 |
New |
Done (2026-04-21) — MR !104 |
7 |
New |
Done (2026-04-21) — MR !104 |
8 |
Extend |
Done (2026-04-21) — MR !104 |
9 |
Plan sync: in |
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/--householdsknobs 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_idcolumn isTEXT— 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 existingApplicationrows from the SNAP pipeline — applications already have a genericprograms_requestedfield. -
Policy changes: no
jurisdiction.toml/citations.tomledits. 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. ReuseHouseholdContext’s existing child / adult person-id lists for `child_person_id/ WIC participant lookups. -
tools/canopy-seed/src/sql.rs— add SQL renderers mirroring existingwrite_snappatterns. -
tools/canopy-seed/src/manifest.rs— add TypeScript entity blocks + helpers using the existingvals()helper pattern. -
tools/canopy-seed/tests/integration.rs— count assertions. -
xtask/src/cmd/seed.rs— add 2 entries toDATABASES. -
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 |
|---|---|---|---|
|
1 |
household_id + a child person_id |
Status: first 2/3 of households get |
|
1 per approved CAPS det |
determination_id |
|
|
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. |
|
1 per approved WIC det |
person_id |
Derives |
|
1 per approved WIC det |
person_id + |
|
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 |
|
Seeder datagen |
|
Seeder SQL rendering |
|
Seeder TS manifest |
|
Seeder tests |
|
xtask wiring |
|
New Playwright specs |
|
Plan Errata |
|
Roadmap |
|
Changelog |
|
No migrations, no service code, no new crates.
Verification
Per-step verification
-
cargo nextest run -p canopy-seed— integration tests assert new-entity counts + FK consistency (Step 8’scaps_wic_fixture_counts). -
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 producescanopy_caps.sql+canopy_wic.sql+ updated TS manifest. -
Manual SQL inspection:
cat /tmp/seed-test/canopy_caps.sqlshould show 9INSERT INTO caps_determinationsrows + 6INSERT INTO caps_authorizationsrows. -
cargo xtask dev restart+cargo xtask seed --seed 42 --households 9— real DB load (devstack path). -
psqlad-hoc viadocker exec -i canopy-postgres-1 psql -U canopy -d canopy_caps -c 'SELECT count(*) FROM caps_determinations;'should return 9. -
cargo xtask e2e — --grep "CAPS case detail"— new Playwright CAPS spec passes. -
cargo xtask e2e — --grep "WIC case detail"— new Playwright WIC spec passes. -
cargo xtask validate— full battery green (fmt + clippy + nextest + docker build). Pre-push hook runs this automatically ifgit config core.hooksPath .githooksis active.
Plan-level verification
-
findCapsApproved()andfindWicApproved()intests/e2e/lib/seed.tsreturn real objects at default seed (--households=9). -
No regressions: existing 101-test E2E suite still passes byte-for-byte (SNAP
findApproved()unchanged). -
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.adocErrata — append "Resolved 2026-04-21 by canopy-seed-caps-wic-fixtures." to the=== 2026-04-21 — Step 8 Playwright E2E deferredsubsection. -
canopy-wic-list-endpoints.adocErrata — same pattern. -
roadmap.adocTier 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):
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.