152 lines
14 KiB
Markdown
152 lines
14 KiB
Markdown
---
|
|
phase: 07-mobile-test-harness
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["07-01"]
|
|
files_modified:
|
|
- apps/pwa/e2e/global-setup.ts
|
|
- apps/pwa/e2e/README.md
|
|
autonomous: true
|
|
requirements: [TEST-02]
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "global-setup polls baseURL+/health and only proceeds once it returns 200 (fails fast with a clear message on timeout)"
|
|
- "global-setup deterministically resets (TRUNCATE) then seeds: >=1 calendar_event on calendar_id=10, >=1 list owned by user 1, >=2 list_items, >=1 list_shares row for user 1 (D-05 populated half / D-07 seeding in global-setup)"
|
|
- "Seeding is idempotent run-over-run (a second run produces the same row counts, no stale rows, no duplicate-key errors)"
|
|
- "calendar_id=10 is guaranteed present via INSERT IGNORE INTO calendars before the event insert (works on a fresh CI DB and a populated dev DB)"
|
|
artifacts:
|
|
- path: "apps/pwa/e2e/global-setup.ts"
|
|
provides: "Playwright globalSetup: /health readiness poll + mysql2 reset-and-seed against dev MariaDB"
|
|
contains: "TRUNCATE"
|
|
- path: "apps/pwa/e2e/README.md"
|
|
provides: "Operator/CI run instructions + the DEV_AUTH_BYPASS / NODE_ENV production guardrail documentation"
|
|
contains: "DEV_AUTH_BYPASS"
|
|
key_links:
|
|
- from: "apps/pwa/e2e/global-setup.ts"
|
|
to: "dev MariaDB :3306"
|
|
via: "mysql2 createConnection with DB_* env vars"
|
|
pattern: "mysql.*createConnection"
|
|
- from: "apps/pwa/e2e/global-setup.ts"
|
|
to: "calendar_events.calendar_id=10"
|
|
via: "INSERT IGNORE calendars guard then INSERT calendar_events"
|
|
pattern: "INSERT IGNORE INTO calendars"
|
|
---
|
|
|
|
<objective>
|
|
Create the Playwright `globalSetup` (seeding runs in global-setup per D-07) that the harness runs once before any spec: poll the PWA `/health` endpoint until the dev stack is ready (D-08 readiness gate), then deterministically reset-and-seed the dev MariaDB (D-06) so dev-bypass user 1 — who natively has no calendars or lists — renders populated calendar and list views. This is the populated half of the D-05 hybrid strategy (the explicit empty-state assertions live in Plan 04). Document the run/CI invocation and the `DEV_AUTH_BYPASS`/production guardrail.
|
|
|
|
Purpose: Without seeding, user 1's views are empty and the "populated state" assertions (UI-SPEC Rules 3/5) have nothing to assert against. Without the readiness poll, specs flake on ECONNREFUSED when the stack is still booting (especially in Phase 8 CI). This is the data + readiness precondition every spec plan depends on (TEST-02).
|
|
|
|
Output: `apps/pwa/e2e/global-setup.ts` and `apps/pwa/e2e/README.md`.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/07-mobile-test-harness/07-CONTEXT.md
|
|
@.planning/phases/07-mobile-test-harness/07-RESEARCH.md
|
|
@.planning/phases/07-mobile-test-harness/07-PATTERNS.md
|
|
@.planning/phases/07-mobile-test-harness/07-VALIDATION.md
|
|
@apps/pwa/playwright.config.ts
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: global-setup.ts — /health readiness poll + deterministic reset-and-seed</name>
|
|
<files>apps/pwa/e2e/global-setup.ts</files>
|
|
<read_first>
|
|
- .planning/phases/07-mobile-test-harness/07-PATTERNS.md § "apps/pwa/e2e/global-setup.ts" — full global-setup shape, exact column names, the INSERT IGNORE calendars guard, the seed SQL
|
|
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Pattern 2" + § "Pitfall 4" + § "Pitfall 2" — health poll, seed, calendar_id=10 FK guard, globalSetup has no Playwright fixtures (plain Node only)
|
|
- apps/api/src/db/client.ts — EXACT mysql2 env-var names to reuse: `DB_HOST` (default 127.0.0.1, NOT localhost — per memory api-integration-test-db), `DB_PORT` (3306), `DB_USER` (familysync), `DB_PASSWORD`, `DB_NAME` (familysync)
|
|
- apps/api/src/db/schema.ts — confirm exact column names: calendars(`id`,`user_id`,`url`,`display_name`,`color`,`is_shared`); calendar_events(`calendar_id`,`uid`,`etag`,`raw_vevent`,`title`,`dtstart_utc` TIMESTAMP,`dtstart_date` DATE,`all_day`,`has_rrule`); lists(`id`,`owner_id`,`name`,`is_shared`); list_shares(`list_id`,`user_id`); list_items(`list_id`,`text`,`checked`,`rank`)
|
|
- apps/api/src/auth/devBypass.ts — DEV_USER.id === 1 (seed targets user_id=1 / owner_id=1)
|
|
- apps/api/tests/routes/lists.test.ts — existing seed-helper INSERT shapes for lists/list_items/list_shares (Drizzle there; global-setup uses raw mysql2 but the table/column shape is identical)
|
|
</read_first>
|
|
<action>
|
|
Create `apps/pwa/e2e/global-setup.ts` exporting a default async function (Playwright globalSetup signature; seeding-in-global-setup is D-07). Use ONLY plain Node APIs — `fetch` (native in Node 22) and `mysql2/promise` — NO `@playwright/test` imports (globalSetup runs outside the worker context; importing `page`/`test` throws — Pitfall 2). Step 1 (readiness, D-08): read `baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'`; loop with a 60s deadline polling `fetch(baseURL + '/health')`, break on `res.ok`, swallow ECONNREFUSED, sleep 1000ms between attempts; if the deadline passes without a 200, throw an Error with a clear message (e.g. ``health check never returned 200 at ${baseURL}/health — is the dev stack up?``) so the run fails fast. Step 2 (seed the populated half of the D-05 hybrid, deterministically per D-06): `mysql.createConnection` using the exact env-var names from db/client.ts (`DB_HOST` default `'127.0.0.1'`, `DB_PORT` 3306, `DB_USER` familysync, `DB_PASSWORD`, `DB_NAME` familysync). In a try/finally (finally calls `conn.end()`): `SET FOREIGN_KEY_CHECKS=0`; TRUNCATE in FK-safe order `list_items`, `list_shares`, `lists`, `calendar_events`; `SET FOREIGN_KEY_CHECKS=1`. Then the calendar_id=10 FK guard (Pitfall 4): `INSERT IGNORE INTO calendars (id, user_id, url, display_name, color, is_shared) VALUES (10, 1, 'https://caldav.fastmail.com/dav/calendars/e2e/', 'FamilySync', '#4A90D9', true)` — IGNORE makes it a no-op on the dev DB where row 10 already exists and creates it on a fresh CI DB. Then seed one TIMED (not all-day) calendar_event onto calendar_id=10 with a deterministic uid `'e2e-seed-event-001'`, title `'Seeded Test Event'`, a future `dtstart_utc` (ISO UTC string ~tomorrow), a minimal VCALENDAR/VEVENT `raw_vevent`, `etag='e2e-etag-001'`, `all_day=false`, `has_rrule=false`. Then seed one list `INSERT INTO lists (owner_id, name, is_shared) VALUES (1, 'E2E Grocery List', true)`, capture `insertId` as `listId`, then `INSERT INTO list_shares (list_id, user_id) VALUES (?, 1)` and `INSERT INTO list_items (list_id, text, checked, rank) VALUES (?, 'Milk', false, 'a0'), (?, 'Eggs', false, 'a1')`. Use fractional-indexing rank strings `'a0'`/`'a1'` (active items render top-section). Seed BOTH `lists.owner_id=1` AND a `list_shares` row (Open Question 3 — belt-and-suspenders so `/api/lists` returns the list whether it filters by owner or by share). Determinism note: because every run truncates first, a second run yields identical row counts — no INSERT IGNORE on the seed rows themselves (D-06 mandates truncate+insert, NOT insert-if-absent). The list name `'E2E Grocery List'` and item texts `'Milk'`/`'Eggs'` are the stable anchors the lists spec (Plan 04) asserts on — do not change them without updating that spec.
|
|
</action>
|
|
<acceptance_criteria>
|
|
- `apps/pwa/e2e/global-setup.ts` imports `mysql2/promise` and contains NO `@playwright/test` import
|
|
- it polls `${baseURL}/health` in a bounded loop and throws on timeout
|
|
- it executes TRUNCATE on list_items, list_shares, lists, calendar_events (FK checks toggled around it)
|
|
- it contains `INSERT IGNORE INTO calendars` with `VALUES (10, 1, ...)` before the calendar_events insert
|
|
- it inserts a calendar_event with `calendar_id` 10, a list owned by user 1, a list_shares row (user_id 1), and exactly two list_items (`Milk`, `Eggs`)
|
|
- running it twice in a row against the dev DB leaves exactly: 1 calendar_event on cal 10, 1 list, 1 list_shares, 2 list_items (idempotent) — verify with the SQL count in the verify block
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>cd apps/pwa && PLAYWRIGHT_BASE_URL="${PLAYWRIGHT_BASE_URL:-http://localhost:5173}" node --import tsx -e "import('./e2e/global-setup.ts').then(m=>m.default()).then(async()=>{const mysql=await import('mysql2/promise');const c=await mysql.createConnection({host:process.env.DB_HOST??'127.0.0.1',port:Number(process.env.DB_PORT??3306),user:process.env.DB_USER??'familysync',password:process.env.DB_PASSWORD??'',database:process.env.DB_NAME??'familysync'});const[e]=await c.query('SELECT COUNT(*) n FROM calendar_events WHERE calendar_id=10');const[li]=await c.query('SELECT COUNT(*) n FROM list_items');console.log('events_cal10=',e[0].n,'list_items=',li[0].n);await c.end();process.exit((e[0].n>=1&&li[0].n>=2)?0:1)})"</automated>
|
|
</verify>
|
|
<done>global-setup polls /health, then deterministically resets and seeds calendar_id=10 (with the INSERT IGNORE guard), one shared list owned by user 1, a list_shares row, and two list_items — idempotent run-over-run.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: e2e/README.md — run instructions + DEV_AUTH_BYPASS / production guardrail</name>
|
|
<files>apps/pwa/e2e/README.md</files>
|
|
<read_first>
|
|
- .planning/phases/07-mobile-test-harness/07-RESEARCH.md § "Security Domain" + § "Pitfall 5" — the DEV_AUTH_BYPASS production guardrail and propagation requirement
|
|
- .planning/phases/07-mobile-test-harness/07-CONTEXT.md § "D-09" — stack bring-up is the caller's responsibility (operator locally, compose in CI)
|
|
- .planning/phases/07-mobile-test-harness/07-VALIDATION.md § "Manual-Only Verifications" — the auth-bypass-off manual check
|
|
- docs/deployment.md (the "Running locally (host-side, no Docker)" subsection) — the canonical local dev-stack bring-up command to reference, not duplicate
|
|
</read_first>
|
|
<action>
|
|
Create `apps/pwa/e2e/README.md` documenting how to run the harness and the security guardrails. Cover: (1) Prerequisites — the caller brings up the dev stack first (D-09): API + PWA dev servers + dev MariaDB (:3306) + Redis, with `DEV_AUTH_BYPASS=true` set in the API's environment BEFORE the API starts (Pitfall 5 — the harness cannot set it; it must already be active for the API to resolve to Dev User id 1). Reference docs/deployment.md's host-side run command rather than copying it. (2) Run commands: full suite `pnpm --filter @familysync/pwa test:e2e`; single fast profile `pnpm --filter @familysync/pwa exec playwright test --project=pixel`; headed debug `--headed`. (3) Env vars the harness reads: `PLAYWRIGHT_BASE_URL` (default http://localhost:5173), `DB_HOST` (default 127.0.0.1), `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` — all DB creds come from env, NEVER hardcoded (Information Disclosure mitigation). (4) SECURITY GUARDRAIL (Elevation of Privilege): `DEV_AUTH_BYPASS=true` is dev-only — the API guards it behind `NODE_ENV !== 'production'`; the production compose file MUST NOT set `DEV_AUTH_BYPASS`. State this explicitly. (5) No `storageState`: the harness never reads or writes a session-state file (D-01); there is no expiring cookie to refresh, which is why it runs repeatably day-over-day (SC #3). (6) Note that Phase 8 CI consumes these specs unchanged and owns only the stack bring-up + readiness wait. Keep it concise — this is operator-facing reference, not a tutorial.
|
|
</action>
|
|
<acceptance_criteria>
|
|
- `apps/pwa/e2e/README.md` exists and documents the `pnpm --filter @familysync/pwa test:e2e` run command
|
|
- it states `DEV_AUTH_BYPASS=true` must be set before the API starts and MUST NOT be set in production (NODE_ENV !== 'production' guard)
|
|
- it lists the DB_* and PLAYWRIGHT_BASE_URL env vars and states DB creds are env-only (never hardcoded)
|
|
- it states no storageState file is used (D-01)
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>grep -E 'DEV_AUTH_BYPASS|NODE_ENV|storageState|PLAYWRIGHT_BASE_URL|test:e2e' apps/pwa/e2e/README.md | grep -vc '^#'</automated>
|
|
</verify>
|
|
<done>e2e/README.md documents run commands, the env-var contract, and the DEV_AUTH_BYPASS/production + no-storageState guardrails.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| global-setup → dev MariaDB | direct mysql2 connection writes seed rows; credentials cross this boundary |
|
|
| harness → dev API | the API runs with `DEV_AUTH_BYPASS=true`; the bypass must never be active in production |
|
|
| repo → production | README + seed code checked into the repo; must not normalize the dev-bypass posture for production |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-07-04 | Elevation of Privilege | `DEV_AUTH_BYPASS=true` leaking to production | mitigate | README documents that the bypass is dev-only and guarded by `NODE_ENV !== 'production'` (devBypass.ts); production compose MUST NOT set `DEV_AUTH_BYPASS`. global-setup does not set it (it cannot — it must already be active on the API). |
|
|
| T-07-05 | Information Disclosure | DB seed credentials | mitigate | global-setup reads `DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD`/`DB_NAME` from env exclusively (mirrors db/client.ts); no credential is hardcoded in the seed script or README. |
|
|
| T-07-06 | Information Disclosure | checked-in OIDC session state | accept (designed out) | N/A by D-01 — no `storageState.json` is written; the seed touches only fixture rows, never an auth artifact. README states this explicitly. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- global-setup polls /health and fails fast on timeout.
|
|
- Reset-and-seed yields >=1 calendar_event on calendar_id=10 and >=2 list_items, idempotent across two consecutive runs.
|
|
- INSERT IGNORE calendars guard satisfies the FK on both a fresh CI DB and the populated dev DB.
|
|
- README documents the DEV_AUTH_BYPASS/production guardrail, the env-only DB creds, and no-storageState.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `apps/pwa/e2e/global-setup.ts` provides a readiness gate + deterministic reset-and-seed using only plain Node (fetch + mysql2), targeting user 1 / calendar 10.
|
|
- Seeding is repeatable run-over-run with no stale rows or duplicate-key failures.
|
|
- `apps/pwa/e2e/README.md` documents run, env, and security guardrails.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/07-mobile-test-harness/07-02-SUMMARY.md` when done.
|
|
</output>
|