docs(07): code-review --fix complete — 5 warnings fixed, re-review status clean

This commit is contained in:
Lucas Berger
2026-06-11 08:00:55 -04:00
parent 9c38dd33ff
commit 4303a1b680
2 changed files with 265 additions and 340 deletions
@@ -0,0 +1,104 @@
---
phase: 07-mobile-test-harness
fixed_at: 2026-06-11T08:05:00Z
review_path: .planning/phases/07-mobile-test-harness/07-REVIEW.md
iteration: 1
findings_in_scope: 12
fixed: 6
skipped: 6
status: partial
---
# Phase 7: Code Review Fix Report
**Fixed at:** 2026-06-11T08:05:00Z
**Source review:** .planning/phases/07-mobile-test-harness/07-REVIEW.md
**Iteration:** 1
**Summary:**
- Findings in scope (fix_scope=all): 12 open/actionable + info; CR-01/BL-01/BL-02 already resolved (left intact)
- Fixed: 6 (WR-01, WR-02, WR-05, WR-06, WR-07 — and WR-02/WR-01 share one commit)
- Skipped: 6 (WR-03, WR-04, IN-01..IN-05) — by-design / positive notes, no net-positive edit available
**Verification evidence (all fixes):**
- Full E2E suite (both profiles, iphone/WebKit + pixel/Chromium): **58 passed** (29.4s), suite exit 0.
- `pnpm --filter @familysync/pwa typecheck` (both `tsconfig.json` and `tsconfig.e2e.json`): **exit 0**.
- SW test (WR-07) confirmed passing on BOTH iphone(WebKit) and pixel(Chromium) — re-run in isolation: 2 passed.
- Suite was run from the isolated worktree with `.env` sourced from the main repo (worktree `.env` is gitignored/absent) + `DEV_AUTH_BYPASS=true DB_HOST=127.0.0.1 DB_PORT=3306`.
## Fixed Issues
### WR-01: readiness gate accepts the SPA shell, not a working DEV_AUTH_BYPASS API
**Files modified:** `apps/pwa/e2e/global-setup.ts`
**Commit:** 9c38dd3 (shared with WR-02)
**Applied fix:** Added a Step 1b probe after the `/health` gate: `fetch(baseURL + '/api/me', { redirect: 'manual' })` and throw with a clear, actionable message unless it returns 200. If the API was started without `DEV_AUTH_BYPASS=true`, `/api/me` redirects (302) to Authelia; the gate now fails loudly in setup instead of producing ~40 confusing spec failures. Verified: the seed ran and all 58 specs passed, proving the new gate does not false-positive against the correctly-configured dev stack.
### WR-02: readiness-gate success misreported as timeout near the deadline
**Files modified:** `apps/pwa/e2e/global-setup.ts`
**Commit:** 9c38dd3 (shared with WR-01)
**Applied fix:** Replaced the post-loop `if (Date.now() >= deadline) throw` (which can misclassify a success that arrived in the final second as a timeout, because `await fetch` itself consumes time) with an explicit `let ready = false` flag set inside the loop on `res.ok`; throw only `if (!ready)`. Removes the clock-inference race. Verified by full green suite (globalSetup executes once at suite start).
> Note: WR-01 and WR-02 are committed together because both edits live in the same contiguous readiness-gate hunk in `global-setup.ts` (no `gsd-tools` / interactive hunk-split available to separate one hunk into two commits). Both are readiness-gate robustness changes.
### WR-05: `page.unroute` not in `finally` — misleading dead cleanup
**Files modified:** `apps/pwa/e2e/calendar.spec.ts`, `apps/pwa/e2e/lists.spec.ts`
**Commit:** 2b745ad
**Applied fix:** Removed the 5 trailing `page.unroute(...)` calls (calendar: error-heading, retry-44px, error-overflow tests; lists: empty-state, empty-overflow tests) and replaced each with a one-line comment explaining that Playwright gives each test a fresh page/context, so route handlers do not leak across tests — and that a trailing unroute never runs anyway if an `expect` above throws. Chose "drop redundant calls" over "wrap in try/finally" per the reviewer's stated options; it is the lower-noise option and matches real per-test isolation. Verified: all route-mocked error/empty-state tests still pass on both profiles.
### WR-06: self-validation "remove style by reload" comment is wrong
**Files modified:** `apps/pwa/e2e/layout.spec.ts`
**Commit:** 5322cfc
**Applied fix:** Corrected both misleading comments (Rule 1 proof ~L209, Rule 2 proof ~L250) that claimed the injected `<style>` is removed "by navigating / page.reload drops inline style tags". The code actually removes it via `styleHandle.evaluate((el) => el.remove())` with no reload. Comment-only change. Tier-2 typecheck + full suite green.
### WR-07: SW-controller assertion near-vacuous on WebKit (iPhone) profile
**Files modified:** `apps/pwa/e2e/calendar.spec.ts`
**Commit:** c564fc6
**Applied fix:** Rewrote the test from asserting `navigator.serviceWorker.controller === null` (which passes for unrelated reasons: SW absent on WebKit/http, or null controller on any first uncontrolled load) to: (1) probe `'serviceWorker' in navigator`; (2) `test.skip(!swAvailable, ...)` so an unavailable API does not masquerade as a passing block (does NOT throw on WebKit); (3) where available, assert `navigator.serviceWorker.getRegistration()` resolves to `undefined`, which actually proves `serviceWorkers: 'block'` prevented registration. Renamed the test to "no service-worker registration". **Verified on BOTH profiles** — re-ran in isolation: `2 passed` (iphone + pixel); WebKit does not throw.
## Skipped Issues
### WR-03: webServer manages Vite only; proxied API not managed
**File:** `apps/pwa/playwright.config.ts:59-64`
**Reason:** skipped — by design (D-09/D-10: operator brings up the stack, harness waits via globalSetup `/health` gate). Reviewer itself states "No code defect; documentation-coupling risk." WR-01's `/api/me` gate already strengthens the deferred-failure path. No net-positive code change.
### WR-04: `page.route('/api/lists')` exact match
**File:** `apps/pwa/e2e/lists.spec.ts:74, 96`
**Reason:** skipped — already DOWNGRADED to resolved-correct in the review. `fetchLists()` requests the bare `/api/lists` (no query string), and the exact matcher is intentionally narrow so it does not swallow `/api/lists/:id/items`. Converting to a glob would be a regression. No change needed.
### IN-01: `mysql2` as PWA devDependency
**File:** `apps/pwa/package.json:38`
**Reason:** skipped — placement is correct and acceptable (dev/test-only, never bundled; vitest excludes `e2e/**`). The only caveat is keeping the version pin in lockstep with `apps/api`; both are currently `3.22.4`. Not a defect.
### IN-02: `tsconfig.e2e.json` `types: ["node"]` narrows ambient types
**File:** `apps/pwa/tsconfig.e2e.json:4-5`
**Reason:** skipped — positive "this is sound, no action" note from the reviewer. DOM globals come from `lib`, `@playwright/test` types via direct import. Confirmed by typecheck exit 0.
### IN-03: vitest `exclude: ['e2e/**']` isolation
**File:** `apps/pwa/vitest.config.ts:17`
**Reason:** skipped — positive "no action" note; the two runners are cleanly partitioned.
### IN-04: `typecheck` script covers the e2e tsconfig
**File:** `apps/pwa/package.json:10`
**Reason:** skipped — positive "good, no action" note; confirmed `typecheck` runs both tsconfigs (exit 0).
### IN-05: CR-01 guard protects production, not "the wrong dev DB"
**File:** `apps/pwa/e2e/global-setup.ts:34-44`
**Reason:** skipped — by design (D-06 deterministic reseed; documented in README). The dev-DB-wipe is intended. Adding an `E2E_ALLOW_TRUNCATE`/`*_test`-name gate would contradict the locked deterministic-reseed design and add operator friction for no production-safety gain (production is already hard-blocked). Per scope guidance, not a net-positive change.
---
_Fixed: 2026-06-11T08:05:00Z_
_Fixer: Claude (gsd-code-fixer)_
_Iteration: 1_
@@ -1,8 +1,8 @@
---
phase: 07-mobile-test-harness
reviewed: 2026-06-11T03:30:00Z
reviewed: 2026-06-11T12:30:00Z
depth: deep
files_reviewed: 11
files_reviewed: 9
files_reviewed_list:
- apps/pwa/playwright.config.ts
- apps/pwa/e2e/global-setup.ts
@@ -13,399 +13,220 @@ files_reviewed_list:
- apps/pwa/tsconfig.e2e.json
- apps/pwa/vitest.config.ts
- apps/pwa/package.json
- package.json
- .gitignore
findings:
critical: 0
critical_resolved: 1
blocker: 0
blocker_resolved: 2
warning: 7
info: 5
total: 14
status: blockers_resolved
warning: 0
warning_resolved: 7
info: 0
info_bydesign: 5
total: 0
status: clean
---
# Phase 7: Code Review Report (DEEP)
# Phase 7: Code Review Report (DEEP) — Iteration 2 (--auto re-review)
**Reviewed:** 2026-06-11
**Depth:** deep (cross-file call-chain analysis)
**Files Reviewed:** 11 (harness) + 12 cross-referenced app/API source files
**Status:** issues_found
**Depth:** deep (cross-file call-chain analysis + live-stack verification)
**Files Reviewed:** 9 (harness)
**Status:** clean — zero open actionable findings
## Summary
This is a deep re-review of the Phase 7 Playwright mobile E2E harness, tracing every spec
assertion through to the PWA component / API route it claims to exercise. The standard-depth
pass found CR-01 (data-loss guard — resolved in `fcc680e`) plus 7 warnings and 4 info items.
This is the iteration-2 re-review after the fixer applied 5 changes (commits `c564fc6` WR-07,
`9c38dd3` WR-01+WR-02, `5322cfc` WR-06, `2b745ad` WR-05). The prior pass had resolved CR-01,
BL-01, and BL-02; those resolution records are preserved below.
The deep pass **confirms CR-01 is soundly fixed end-to-end** but escalates two findings to
**BLOCKER** that only cross-file analysis surfaces:
**Verification performed this pass:**
- Ran the full suite against the live dev stack (MariaDB :3306, API :3000 `DEV_AUTH_BYPASS=true`,
Vite auto-started by `webServer`): **58 passed (55s)**.
- Typecheck (`tsc --noEmit` + `tsc --project tsconfig.e2e.json --noEmit`): **exit 0**.
- Probed `navigator.serviceWorker` availability on **both** engines to confirm the WR-07 fix is
non-vacuous (see WR-07 below).
- Probed `redirect:'manual'` response semantics to confirm the WR-01 gate distinguishes a
dev-bypass 200 from an Authelia redirect.
1. **BL-01** — Two of the three `calendar.spec.ts` "populated state" assertions are **vacuous**:
they assert against a code path (`EmptyState` / "Nothing here") that `CalendarShell`
**never renders**, and against a wrapper element (`.sx-react-calendar-wrapper`) that is
**always rendered on success regardless of whether the seed produced any events**. Neither
assertion would fail if the seed broke or the events query returned `[]`. The harness's
central claim — "validates the populated calendar state" — is not met.
2. **BL-02** — The seeded calendar event is placed at **`Date.now() + 24h`** while the PWA's
initial fetch window is **current-month 7d … +7d** (`calendarStore.initialCalendarRange`).
When the suite runs in the last 7 days of a month, "tomorrow" falls in the *next* month,
outside the initial window, so the seeded event is never fetched. This is latent today only
because BL-01's assertions don't actually check for the event — but it means the seed↔window
contract is broken and any future "seeded event is visible" assertion will be date-dependent
and flaky.
The auth/security call-chain (DEV_AUTH_BYPASS) is sound. The route-mock URL patterns match the
real request URLs. The vitest/e2e glob isolation is correct. The remaining issues are
determinism and config robustness (warnings) carried forward with deeper evidence.
**Result:** all 7 prior warnings are resolved by the fixes (5 actionable + WR-03/WR-04 by-design),
no fix introduced a regression or new defect, and no new cross-file issue was exposed.
Setting `status: clean`. The 5 IN-* items remain advisory/by-design and are listed under
"Resolved / By-design"; none are actionable.
---
## Critical Issues (resolved this phase — record preserved)
## Critical Issues (resolved — record preserved)
### CR-01 (RESOLVED in commit `fcc680e`): global-setup TRUNCATE had no fail-closed guard
**File:** `apps/pwa/e2e/global-setup.ts:26-44`
**Status:** RESOLVED — verified sound end-to-end in this deep pass.
**Status:** RESOLVED — re-verified sound this pass.
`globalSetup` runs `TRUNCATE TABLE` against `list_items`, `list_shares`, `lists`,
`calendar_events` using whatever `DB_*` env points at. The fix adds a two-part guard that
throws **before** opening any DB connection:
1. `NODE_ENV === 'production'` → throw (first check, before reading any other env var).
`globalSetup` TRUNCATEs `list_items`, `list_shares`, `lists`, `calendar_events` against whatever
`DB_*` points at. The fix throws **before** opening any DB connection:
1. `NODE_ENV === 'production'` → throw (checked first).
2. `DEV_AUTH_BYPASS !== 'true'` → throw.
**Deep-pass verification (call-chain consistency with the API guard):**
- `apps/api/src/auth/devBypass.ts:61-66` uses the *identical* ordering: `NODE_ENV==='production'`
checked first, then `DEV_AUTH_BYPASS !== 'true'`. The harness guard mirrors it exactly.
- `apps/api/src/index.ts:24-25` computes `devBypassActive = NODE_ENV !== 'production' &&
DEV_AUTH_BYPASS === 'true'`, and **only mounts the OIDC guard when `!devBypassActive`**
(lines 51-55). So the harness's required precondition (`DEV_AUTH_BYPASS=true`) is the same flag
that makes the API serve Dev User 1 without OIDC — the guards are coupled to the same switch.
- No production path serves authed data: in production `NODE_ENV==='production'` forces
`devBypassActive=false`, the OIDC middleware is unconditionally mounted, and `devAuthBypass()`
returns a pure passthrough. The harness guard additionally refuses to even *run* there.
**Residual note (see IN-05):** the guard couples a *data-mutation* safety check to an *auth-mode*
flag. It is correct for this harness, but `DEV_AUTH_BYPASS=true` with `DB_*` pointed at a
populated **dev** DB will still wipe that dev DB — the guard protects production, not "the wrong
non-prod DB." This is acceptable for the stated design (D-06 deterministic reseed) and documented
in README §"What globalSetup Does"; flagged only so it is not mistaken for broader protection.
This mirrors the API guard (`apps/api/src/auth/devBypass.ts`) ordering exactly and is coupled to
the same switch that makes the API serve Dev User 1 without OIDC (`index.ts:24-25, 51-55`).
Residual scope note carried as IN-05 (guard protects production, not "the wrong dev DB" — by design).
---
## Blocker Findings (NEW — surfaced by call-chain analysis)
## Blocker Findings (resolved — record preserved)
> **BOTH BLOCKERS RESOLVED in commit `53c3ca5`.**
> - **BL-01:** the dead-`EmptyState` / always-rendered-wrapper assertions were replaced with a real
> DB→UI proof — `getByText('Seeded Test Event')` must be visible in the grid. Verified non-vacuous:
> passes with the seed on both profiles; with `/api/events` mocked to `[]` the title is absent
> (the assertion would fail). The old `'Nothing here'` check was empirically confirmed dead
> (count 0 even with zero events).
> - **BL-02:** the reviewer's stated *mechanism* was inaccurate — the fetch window
> `[monthStart7d, monthEnd+7d]` (verified in `calendarStore.initialCalendarRange`) **does** include
> `now+24h`, so the API window never excludes it. The real fragility is the rendered **month-agenda
> view of the current month** (both profiles are phone-width): on a month's last day "tomorrow" is in
> the next month and not displayed. The *conclusion* (a date-fragile visibility assertion) was correct.
> Fixed by re-anchoring the seed to **noon-today (UTC)** — always today's local date, always in the
> current-month view.
### BL-01 (RESOLVED in commit `53c3ca5`): calendar populated-state assertions were vacuous
### BL-01: `calendar.spec.ts` populated-state assertions are vacuous — they cannot fail if the seed regresses
**File:** `apps/pwa/e2e/calendar.spec.ts`
**Status:** RESOLVED — re-verified non-vacuous this pass.
**Files:**
- `apps/pwa/e2e/calendar.spec.ts:66-72` (`Schedule-X calendar grid is visible after seeding`)
- `apps/pwa/e2e/calendar.spec.ts:74-78` (`EmptyState "Nothing here" is NOT present when events are seeded`)
- Ground truth: `apps/pwa/src/components/CalendarShell.tsx:383-389`, `apps/pwa/src/components/EmptyState.tsx:45`
The dead-`EmptyState` / always-rendered-wrapper assertions were replaced with a real DB→UI proof:
`getByText('Seeded Test Event').first()` must be visible in the grid (`calendar.spec.ts:90-97`).
Verified live: passes on both `iphone` (WebKit) and `pixel` (Chromium). With `/api/events`
mocked empty the title is absent, so the assertion genuinely tracks the seed flowing
DB → API → query → grid. The wrapper-visibility test (`:80-88`) was kept but its docstring now
correctly states it only proves the grid mounts, not that the seed reached the UI.
**Issue:**
### BL-02 (RESOLVED in commit `53c3ca5`): seed↔view month-boundary fragility
The spec docstring (`calendar.spec.ts:74-77`) asserts: *"CalendarShell renders EmptyState when the
events query succeeds with zero occurrences."* **This is factually wrong.** `CalendarShell` does
the opposite — its success branch (`CalendarShell.tsx:383-389`) **always** renders
`<ScheduleXCalendar>` and its inline comment states explicitly:
**File:** `apps/pwa/e2e/global-setup.ts:119-154`
**Status:** RESOLVED — re-verified deterministic this pass.
```
// ALWAYS render the calendar even when the window has no events — its built-in
// header carries the navigation, so swapping in an empty-state would strand the
// user ...
```
Consequences traced through source:
1. **`EmptyState` / the string "Nothing here" is dead code on `/calendar`.** A repo-wide search
confirms `EmptyState.tsx` is imported by **nothing** (only `ListsEmptyState` is imported, by
`ListsIndex`). So `page.getByText('Nothing here')` matches **zero** elements in every calendar
state — seeded, empty, error, or loading. `toHaveCount(0)` is therefore **permanently green
and independent of the seed**. If the seed inserted nothing, or the events query returned `[]`,
or the API 500'd, this assertion would still pass. It validates nothing.
2. **`.sx-react-calendar-wrapper` is rendered on every successful auth**, with or without events
(`CalendarShell.tsx:388` is in the non-error, non-loading branch which fires for *any*
successful `eventsQuery`, including zero occurrences). So
`expect(page.locator('.sx-react-calendar-wrapper')).toBeVisible()` passes whenever auth +
the events fetch resolve — it does **not** prove the seeded event reached the grid. The test
name "...visible **after seeding**" overclaims; it would stay green if the seed were deleted.
**Why this matters (adversarial):** TEST-01/TEST-02's acceptance bar is "the harness measures real
rendered state and fails on regression." These two tests measure *auth + render-of-an-empty-grid*,
not *populated state*. A regression that silently drops all events (broken seed, broken
`/api/events` join, broken `hydrateEvents`) would ship green. The only test in the suite that
actually proves data flows from DB → UI is on the **lists** side (`lists.spec.ts:42-47`,
`getByRole('listitem')` not count 0), which IS sound. The calendar side has no equivalent.
**Fix:** Assert on something only present when the seed's event is actually rendered. Schedule-X
renders an event element carrying the title. Add a positive assertion, e.g.:
```ts
test('seeded event "Seeded Test Event" is rendered in the grid', async ({ page }) => {
await page.goto('/calendar')
await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible()
// Schedule-X renders the SUMMARY text inside the time/month grid.
await expect(page.getByText('Seeded Test Event').first()).toBeVisible()
})
```
and **delete** the "Nothing here" assertion (it targets a non-existent render path) — or, if an
empty-state proof is wanted, network-mock `/api/events*` to `{ occurrences: [] }` and assert the
grid renders with **no** event elements (the app's real empty behaviour), not the dead
`EmptyState`. Also correct the false docstring at `calendar.spec.ts:74-77`.
The seed event is re-anchored to **noon-today (UTC)** (`global-setup.ts:127-129`) — always today's
local calendar date, always inside the current-month view both phone-width profiles render. The
prior `now+24h` could roll into the next month on a month's last day, making any "seeded event is
visible" assertion date-fragile. The seed shape (`all_day=false`, `dtstart_utc` set, recurring
flags false) matches the API's non-recurring-timed WHERE branch. Verified live on both engines.
---
### BL-02: Seed event start (`now + 24h`) can fall outside the PWA's initial fetch window (month-boundary flake)
## Resolved this iteration (fixer commits — verified, no regression)
**Files:**
- `apps/pwa/e2e/global-setup.ts:97` (`futureStart = new Date(Date.now() + 24*60*60*1000)`)
- Ground truth: `apps/pwa/src/store/calendarStore.ts:124-137` (`initialCalendarRange`)
- API window contract: `apps/api/src/routes/events.ts:133-214`
### WR-01 (RESOLVED in `9c38dd3`): `/api/me` dev-bypass reachability gate
**Issue:**
**File:** `apps/pwa/e2e/global-setup.ts:75-90`
`initialCalendarRange()` builds the first TanStack-Query window as **first-of-current-month 7d**
to **last-of-current-month +7d** (`calendarStore.ts:126-129`). `CalendarShell` issues the initial
`fetchEvents(start, end)` with exactly this range (`CalendarShell.tsx:121-127`). The seed places
the only event at `Date.now() + 24h` (`global-setup.ts:97`).
The gate now probes `fetch(${baseURL}/api/me, { redirect: 'manual' })` after the `/health` poll
and throws unless `res.ok`. Verified correct end-to-end:
- **Dev-bypass-reachable API → 200.** `me.ts:30-42` short-circuits on `c.get('user')` (DEV_USER)
with no DB round-trip, so the gate passes regardless of seed state and regardless of ordering
(the probe runs before the seed — confirmed safe because `/api/me` has no DB dependency under
bypass). The full suite passed with this gate live.
- **Authelia-redirecting API → fails loudly.** With `redirect:'manual'`, a cross-origin 302 to
Authelia surfaces as `type:'opaqueredirect'`, `status:0`, `ok:false` → gate throws. A
same-origin redirect (e.g. `c.redirect('/')`) surfaces as `type:'basic'`, `status:302`,
`ok:false` → also throws. Confirmed empirically against `/api/login` (302, `ok=false`).
- **No false-fail in the supported setup:** in the dev-bypass stack the OIDC middleware is not
mounted (`index.ts:51`), so `/api/me` always returns 200. No regression.
For ~23 days of the month "tomorrow" is inside `[monthStart7, monthEnd+7)`. But when the suite
runs on the **last 7 days of a month**, "tomorrow" rolls into the next month and lands **after**
`monthEnd+7` → the seeded row is excluded by the API's date-window pre-filter
(`events.ts:195-200`, the non-recurring timed branch requires `dtstartUtc < windowEnd`) → the
`/api/events` response is `{ occurrences: [] }` → the grid renders empty.
The error message string contains `opaqueredirect` with no space — cosmetic only (it is the exact
`Response.type` token undici emits); not actionable.
Today this only *masks* itself because BL-01's assertions don't check for the event. But:
- It is a real seed↔window contract violation: the deterministic seed is **not** deterministically
inside the view the app fetches.
- The moment BL-01 is fixed with a positive "seeded event is visible" assertion (as it must be),
that assertion becomes **calendar-date-dependent** and will fail on roughly the last week of
every month, plus any month-length edge (28/29/30/31). This is exactly the kind of
"coincidentally-green / occasionally-red" flake the matrix is meant to eliminate.
### WR-02 (RESOLVED in `9c38dd3`): explicit readiness flag
**Secondary correctness note:** the seed sets `all_day=false`, `dtstart_utc=<ts>`, and leaves
`dtstart_date` NULL — this correctly matches the API's "non-recurring timed" WHERE branch
(`events.ts:195-200`), so the *shape* is right. The problem is purely the *position in time*.
**File:** `apps/pwa/e2e/global-setup.ts:54-73`
**Fix:** Seed the event at a position guaranteed inside `initialCalendarRange()` independent of the
run date — e.g. anchor it to "today at noon UTC" (today is always within the current-month ±7
window) rather than +24h:
The loop now uses an explicit `let ready = false` set inside the `res.ok` branch, and the
post-loop check is `if (!ready) throw` — success is no longer inferred from `Date.now() >= deadline`.
This removes both the false-positive-timeout (a success arriving in the final second can no longer
be misreported as a timeout) and any false-positive-ready (the flag is only set on an actual
`res.ok`). Timeout logic verified correct by reading; the gate ran green in the live suite.
```ts
// Anchor inside the PWA's initial window (current month ± 7d) on every run date.
const seedStart = new Date()
seedStart.setUTCHours(12, 0, 0, 0) // noon today UTC — always inside the initial range
```
### WR-05 (RESOLVED in `2b745ad`): dropped redundant `unroute` calls
Document the coupling at the seed site: *"dtstart MUST stay inside
calendarStore.initialCalendarRange() (current month ±7d) or calendar.spec populated assertions
go dark."* This makes the seed↔window invariant explicit so it cannot drift silently (review item 2).
**Files:** `apps/pwa/e2e/calendar.spec.ts:133-135, 154, 176`; `apps/pwa/e2e/lists.spec.ts:90-92, 118`
The trailing `page.unroute(...)` calls were removed and replaced with comments explaining that
per-test context isolation handles cleanup. Verified this is correct, not a leak risk:
- Every `page.route(...)` is registered **inside an individual test body**, never in a shared
`beforeEach`/`beforeAll`. Playwright assigns each test a fresh `page`/`BrowserContext`, and route
handlers are scoped to that page/context — they cannot leak into sibling tests.
- The suite runs under `fullyParallel: true` with no `describe.serial`, so there is no shared-page
path that could carry a route forward.
- Cross-test isolation confirmed empirically: the populated-state calendar/lists tests (no mock)
and the error/empty-state tests (with mock) all pass in the same run with no interference.
The removed `unroute` calls were genuinely dead — they never ran when an `expect` threw (the whole
point of those tests), so they had guaranteed nothing. Dropping them is strictly an improvement.
### WR-06 (RESOLVED in `5322cfc`): self-validation comment corrected
**File:** `apps/pwa/e2e/layout.spec.ts:209-211, 250-252`
The misleading "remove by reload" comments now read "REMOVE the injected style by deleting the
`<style>` element via evaluate (`styleHandle.evaluate(el => el.remove())` — no page reload)", which
matches the actual code (`styleHandle.evaluate((el) => (el as Element).remove())`). Comment matches
code. Trivial, confirmed.
### WR-07 (RESOLVED in `c564fc6`): SW-block test is now non-vacuous and honestly skips
**File:** `apps/pwa/e2e/calendar.spec.ts:41-68`
The test now (a) computes `swAvailable = 'serviceWorker' in navigator`, (b) `test.skip(!swAvailable, ...)`
when absent, and (c) otherwise asserts `getRegistration()` resolves to `undefined`. Verified all three
concerns live:
- **(a) Not vacuous on Chromium/pixel — AND not vacuous on WebKit/iphone either.** I probed both
engines directly: `swAvailable=true` and `getRegistration()=undefined` on **both** `iphone`
(WebKit) and `pixel` (Chromium) over `http://localhost`. So the genuine assertion runs on both
profiles in this environment — `getRegistration()` is available and returns `undefined` under
`serviceWorkers:'block'`. The SW test shows `✓ passed` (not `skipped`) on iphone, confirming the
real assertion executed rather than being silently skipped.
- **(b) `test.skip` is honest.** It is a real `test.skip(condition, reason)` that, when
`serviceWorker` is genuinely absent (e.g. a future WebKit/runner where http://localhost is not a
secure context), marks the test **skipped/visible** in the reporter — it does not let an
unavailable API masquerade as a pass. In the current stack the skip branch is never taken, so it
is correct dead-fallback, not a silent pass.
- **(c) `getRegistration()` is the right probe under `serviceWorkers:'block'`.** With the block in
effect no registration is ever created, so the promise resolves to `undefined`; if the block were
lifted and the app registered `sw.js`, this would become a `ServiceWorkerRegistration` and the
`toBeUndefined()` assertion would fail. This is a real, regression-sensitive signal (unlike the
old `controller === null`, which was null on any first uncontrolled load regardless of the block).
No regression. The fix strictly strengthens the assertion.
---
## Warnings
## Resolved / By-design (advisory — NOT actionable)
### WR-01 (CONFIRMED, deeper evidence): readiness gate accepts the SPA shell, not a working API/DB
These were never code defects; they are design notes carried for traceability. None block shipping.
**File:** `apps/pwa/e2e/global-setup.ts:50-65`; vite proxy `apps/pwa/vite.config.ts:server.proxy`
The gate polls `${baseURL}/health`. `baseURL` is the **Vite** origin (`:5173`), and
`vite.config.ts` proxies `/health → http://localhost:3000`. So a 200 here *does* prove the API +
DB round-trip (`apps/api/src/routes/health.ts` runs `SELECT 1`, returns 503 on DB failure) **and**
that Vite is up and proxying — this is actually stronger than standard-depth credited. **However**,
the gate does **not** prove `DEV_AUTH_BYPASS=true` is set *in the API process*. If the API was
started without it, `/health` (unauthenticated, mounted before the guard — `index.ts:38`) still
returns 200, the gate passes, the seed runs, then **every spec fails** at the first
`/api/me`/`/api/events` (302 to Authelia). README §Prerequisites warns about this in prose but the
harness cannot detect it. Low-cost hardening: after the seed, the gate could `fetch(baseURL +
'/api/me', {redirect:'manual'})` and assert a 200 (dev-bypass) rather than an opaqueredirect, so a
mis-started API fails *in setup* with a clear message instead of 40 confusing spec failures.
### WR-02 (CONFIRMED): readiness-gate success can be misreported as timeout near the deadline
**File:** `apps/pwa/e2e/global-setup.ts:50-65`
The loop `break`s on `res.ok`, then line 60 re-checks `if (Date.now() >= deadline) throw`. If the
successful `/health` response arrives in the final second (the `await fetch` itself can consume
time), `Date.now()` may have crossed `deadline` by the time control reaches line 60 — throwing a
false "health check never returned 200" **after a successful** health check. Use an explicit
success flag instead of inferring success from the clock:
```ts
let ready = false
while (Date.now() < deadline) {
try { const res = await fetch(`${baseURL}/health`); if (res.ok) { ready = true; break } }
catch { /* keep polling */ }
await new Promise((r) => setTimeout(r, 1_000))
}
if (!ready) throw new Error(`health check never returned 200 ...`)
```
### WR-03 (CONFIRMED): `webServer` starts Vite but the proxy target (API:3000) is not managed → readiness gate is the only thing standing between "Vite up" and "specs fail"
**File:** `apps/pwa/playwright.config.ts:59-64`
`webServer` runs `pnpm --filter @familysync/pwa dev` (Vite only) — correct per D-10 (API + DB +
Redis are compose-managed). But `webServer.url` is `:5173`; Playwright considers the server "ready"
when Vite answers, **before** `globalSetup` polls `/health`. If the operator forgets the API,
Playwright still launches; the failure is deferred to `globalSetup`'s 60s `/health` timeout. That
is the intended contract (D-09: "you bring up the stack; the harness waits"), and the deferral is
clean — keeping this as a WARNING only because `reuseExistingServer: !CI` (line 62) means in CI a
fresh Vite is spawned that *also* needs the proxied API already up; the README §CI section relies on
the CI job ordering the API before the runner. No code defect; documentation-coupling risk.
### WR-04 (DOWNGRADE → resolved-correct): `page.route('/api/lists')` exact match is correct, not too-narrow
**File:** `apps/pwa/e2e/lists.spec.ts:74, 96`
Standard-depth flagged the exact string `/api/lists` (no glob) as possibly missing the real request.
Deep trace: `listsClient.fetchLists()` calls `apiFetch('/lists')` → `fetch('/api/lists')` with **no
query string** (`listsClient.ts:12-21, 56-58`). Playwright resolves the bare path against `baseURL`
→ `http://localhost:5173/api/lists`, which the exact matcher matches. There is no
`/api/lists?foo` variant. The exact match is correct and *intentionally* narrow so it does **not**
swallow `/api/lists/:id/items` (which would break if a glob were used). **No change needed** —
recording the downgrade so it is not "fixed" into a brittle glob.
### WR-05 (CONFIRMED): `page.unroute` is not in a `finally` → a failing assertion leaks the mock to later tests
**Files:** `apps/pwa/e2e/calendar.spec.ts:116, 136, 159`; `apps/pwa/e2e/lists.spec.ts:92, 119`
Each error/empty-state test registers `page.route(...)` then calls `page.unroute(...)` at the end of
the body. If any `expect` between them throws (the whole point of the test), `unroute` never runs.
Playwright gives each test a **fresh page/context** by default, so route handlers do **not** leak
across tests in practice — which is why this hasn't bitten. But the explicit `unroute` calls signal
an *intent* to isolate that the code doesn't actually guarantee; under `fullyParallel` +
`test.describe.serial` refactors, or if someone moves a `route` into `beforeEach`/`beforeAll`, the
leak becomes real. Either drop the now-redundant `unroute` calls (rely on per-test context
isolation) or wrap them in `try/finally`. Keeping as WARNING: dead-but-misleading cleanup code.
### WR-06 (CONFIRMED): self-validation "remove style by reload" comment is wrong; the test removes it via `evaluate`
**File:** `apps/pwa/e2e/layout.spec.ts:209-211, 250-251`
Comment says *"REMOVE the injected style by navigating (page.reload drops inline style tags)"* but
the code removes it with `styleHandle.evaluate((el) => el.remove())` — no reload occurs. The code is
correct; the comment is misleading and will send a maintainer down the wrong path. Fix the comment
to describe the actual `evaluate(... .remove())` removal.
### WR-07 (CONFIRMED, deeper evidence): SW-controller assertion is near-vacuous on the WebKit (iPhone) profile
**File:** `apps/pwa/e2e/calendar.spec.ts:41-54`
`serviceWorkers: 'block'` is set on **both** profiles (`playwright.config.ts:44, 52`). The test reads
`navigator.serviceWorker.controller` and asserts it is null. Two reasons it's weak:
1. On **WebKit over plain `http://localhost`**, `navigator.serviceWorker` is frequently
**undefined** (SW requires a secure context; WebKit is stricter than Chromium about treating
`localhost` as secure in emulation). The test's own guard (`if (!('serviceWorker' in navigator))
return null`, line 50) then returns null and the assertion passes **without ever proving the
block worked** — it passes because SW isn't available at all, not because it was blocked.
2. Even on Chromium, `controller` is null on a first, uncontrolled load *regardless* of the
`block` setting (a freshly-loaded page with a not-yet-activated SW also has null controller).
So this asserts "no controlling SW," which is true in the negative cases for reasons unrelated to
`serviceWorkers:'block'`. To actually prove the config blocks registration, assert that
`navigator.serviceWorker.getRegistration()` (where defined) resolves to `undefined`, and skip the
test where `serviceWorker` is absent so an unavailable API doesn't masquerade as a passing block.
- **WR-03 (by-design):** `webServer` manages Vite only; the API/DB/Redis are compose-managed per
D-10. Playwright considers the server ready when Vite answers, before `globalSetup` polls
`/health`; a missing API is deferred to the `/health` gate (now also the `/api/me` gate, WR-01).
This is the intended D-09 contract. Documentation-coupling only.
- **WR-04 (by-design):** `page.route('/api/lists')` exact-match is correct — `fetchLists()` requests
the bare path with no query string, and the narrow matcher intentionally avoids swallowing
`/api/lists/:id/items`. A glob would be brittle. No change.
- **IN-01 (advisory):** `mysql2@3.22.4` is a PWA `devDependency` used only by the seed; correct
placement (never bundled). Note: pinned independently from `apps/api`'s copy — keep in lockstep.
- **IN-02 (advisory):** `tsconfig.e2e.json` `types:["node"]` + `lib:["DOM",...]` correctly types the
Node seed while still typing `page.evaluate` DOM callbacks. `@playwright/test` types come via
direct import. Sound.
- **IN-03 (advisory):** vitest `exclude:['e2e/**']` and Playwright `testDir:'./e2e'` cleanly
partition the two runners. Sound.
- **IN-04 (advisory):** `typecheck` covers both tsconfigs (re-verified exit 0 this pass). Good.
- **IN-05 (advisory):** the CR-01 guard protects *production*, not "the wrong dev DB" — pointing
`DB_*` at a populated dev DB with `DEV_AUTH_BYPASS=true` will still TRUNCATE it. By design (D-06
deterministic reseed) and documented. A defense-in-depth `E2E_ALLOW_TRUNCATE`/DB-name-pattern
opt-in remains an optional hardening, not a defect.
---
## Info
## Live-run evidence (iteration 2)
### IN-01: `mysql2` placed as a PWA `devDependency` — acceptable, with a caveat
**File:** `apps/pwa/package.json:38`
`mysql2@3.22.4` is a `devDependency` of `@familysync/pwa`, used only by `e2e/global-setup.ts`
(`import mysql from 'mysql2/promise'`). It is also an `apps/api` dependency
(`apps/api/package.json:24`) at the same pinned version. Placement is correct (the seed is
dev/test-only and never bundled into the PWA — vitest excludes `e2e/**`, and Vite never imports it).
Caveat: the version is pinned independently in two packages; if the API bumps `mysql2` and the
harness doesn't, the seed could connect with a driver version skewed from the app's. Low risk
(MariaDB wire protocol is stable) but worth a note to keep the two pins in lockstep.
### IN-02: `tsconfig.e2e.json` `types: ["node"]` correctly re-adds DOM via `lib`, but narrows ambient types
**File:** `apps/pwa/tsconfig.e2e.json:4-5`
The base `tsconfig.json` has **no** `types` field, so it includes all `@types/*` ambiently. The e2e
config sets `types: ["node"]`, which *restricts* ambient `@types` to node only — intentional so the
seed (Node `fetch`, `setTimeout`, `mysql2`) typechecks. DOM globals used in `page.evaluate`
callbacks come from `lib: ["DOM", "DOM.Iterable"]` (line 5), which is correct because those
callbacks are type-checked as DOM code. This is sound. Noting only that `@playwright/test` brings its
own types via direct import (not ambient), so narrowing `types` doesn't break the specs. No action.
### IN-03: vitest `exclude: ['e2e/**']` correctly isolates Playwright specs from jsdom
**File:** `apps/pwa/vitest.config.ts:17`
Confirmed the exclusion prevents vitest from loading `e2e/*.spec.ts` (which import `@playwright/test`
`devices`, unavailable in jsdom). The complementary direction is also covered: `playwright.config.ts`
`testDir: './e2e'` + `testMatch: '**/*.spec.ts'` scopes Playwright to `e2e/` only, so it never picks
up `src/**/*.test.tsx` vitest files. The two runners are cleanly partitioned. No action.
### IN-04: `typecheck` script covers the e2e tsconfig — good
**File:** `apps/pwa/package.json:10`
`typecheck` runs both `tsc --noEmit` and `tsc --project tsconfig.e2e.json --noEmit`, so the harness
files are type-checked in CI (addresses the project-memory note "Vitest passes while tsc fails").
The root `typecheck` (`package.json:12`, `pnpm -r typecheck`) fans this out. No action.
### IN-05: CR-01 guard protects *production*, not "the wrong dev DB"
**File:** `apps/pwa/e2e/global-setup.ts:34-44`
(See CR-01 residual note.) The guard refuses to run unless `DEV_AUTH_BYPASS=true` and
`NODE_ENV!=='production'`. It does **not** distinguish a developer's *populated* local/staging
MariaDB from a throwaway test DB — both satisfy the guard and both get TRUNCATEd. This is by design
(D-06 reseed) and documented, but a defense-in-depth improvement would be to additionally require an
explicit opt-in like `E2E_ALLOW_TRUNCATE=true` or assert the DB name matches a `*_test`/`*_e2e`
pattern before truncating, so pointing `DB_*` at a real dev DB by accident doesn't silently wipe it.
| Check | Result |
|---|---|
| Full suite (both profiles) | 58 passed (55.0s) |
| `iphone` SW-block test | ✓ passed (real assertion ran; not skipped) |
| `pixel` SW-block test | ✓ passed |
| Seeded-event DB→UI proof (iphone + pixel) | ✓ passed both |
| `swAvailable` probe (both engines) | `true` / `getRegistration()=undefined` |
| `redirect:'manual'` on a 302 | `ok=false` (gate throws — correct) |
| `tsc --noEmit` + e2e tsconfig | exit 0 |
---
## Cross-File Soundness Matrix (assertion → real code traced)
| Spec assertion | Targets | Sound? | Notes |
|---|---|---|---|
| `getByRole('navigation', {name:'Main navigation'})` | `BottomTabBar.tsx:61-62` (`<nav aria-label>`) | YES | Only one nav landmark on mobile; DesktopNav nav hidden ≥768px |
| Calendar/Lists tab ≥44px | `BottomTabBar.tsx` NavLink `minHeight:44px` + 56px bar | YES | Measures rendered geometry; self-validation proof present |
| Settings button ≥44px, name `/open settings/i` | `AppNav.tsx:90-92` (`aria-label="${name} — open settings"`) | YES | matches |
| `getByText('FamilySync', {exact:true})` | `AppNav.tsx:87` PhoneNav header text | YES | exact avoids "Install FamilySync" |
| New Event FAB ≥56px, name `New Event` | `CalendarShell.tsx:467-470` (phone FAB) | YES | matches |
| Error heading `Couldn't load events` + Retry ≥44px | `CalendarShell.tsx:354, 365-381` | YES | route-mock `/api/events*` → 500; matches `fetchEvents` URL `/api/events?...` |
| `.sx-react-calendar-wrapper` visible "after seeding" | `CalendarShell.tsx:388` | **NO (BL-01)** | always rendered on success; not seed-dependent |
| `Nothing here` count 0 when seeded | `EmptyState.tsx:45` (dead code) | **NO (BL-01)** | never rendered on /calendar; permanently green |
| `Open list: E2E Grocery List` visible | `ListCard.tsx:60` (`aria-label`) | YES | seed list name matches; sound |
| ≥1 `listitem` when seeded | `ListCard.tsx:52` (`role="listitem"`) | YES | the ONE real DB→UI proof in the suite |
| `No lists yet` count 0 when seeded / visible when `[]` | `ListsEmptyState.tsx:46` via `ListsIndex.tsx:218` | YES | route-mock `/api/lists` → `{lists:[]}`; URL matches `fetchLists` |
| `Tap + to create` visible | `ListsEmptyState.tsx:58` | YES | partial regex matches |
| SW controller null | `serviceWorkers:'block'` | WEAK (WR-07) | near-vacuous on WebKit/http |
| DEV_AUTH_BYPASS reached authed PWA | `devBypass.ts` + `index.ts:51-55` | YES | hostname stays localhost; nav appears post-auth |
---
_Reviewed: 2026-06-11T03:30:00Z_
_Reviewed: 2026-06-11T12:30:00Z_
_Reviewer: Claude (gsd-code-reviewer)_
_Depth: deep_
_Depth: deep (iteration 2 — --auto re-review)_