Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
15 KiB
15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-mobile-test-harness | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_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 Task 1: global-setup.ts — /health readiness poll + deterministic reset-and-seed apps/pwa/e2e/global-setup.ts - .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) 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. - `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 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)})" 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. Task 2: e2e/README.md — run instructions + DEV_AUTH_BYPASS / production guardrail apps/pwa/e2e/README.md - .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 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. - `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) grep -E 'DEV_AUTH_BYPASS|NODE_ENV|storageState|PLAYWRIGHT_BASE_URL|test:e2e' apps/pwa/e2e/README.md | grep -vc '^#' e2e/README.md documents run commands, the env-var contract, and the DEV_AUTH_BYPASS/production + no-storageState guardrails.<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>
- 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.<success_criteria>
apps/pwa/e2e/global-setup.tsprovides 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.mddocuments run, env, and security guardrails. </success_criteria>