docs(07): code-review --fix complete — 5 warnings fixed, re-review status clean
This commit is contained in:
@@ -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
|
||||
> `[monthStart−7d, 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 `[monthStart−7, 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)_
|
||||
|
||||
Reference in New Issue
Block a user