---
phase: 3
slug: event-write-back-pwa-install
status: draft
nyquist_compliant: true
wave_0_complete: false
created: 2026-06-05
---

# Phase 3 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.
> Sourced from 03-RESEARCH.md §Validation Architecture.

---

## Test Infrastructure

| Property              | Value                                           |
| --------------------- | ----------------------------------------------- |
| **Framework (API)**   | Vitest 4.x, environment: `node`                 |
| **Framework (PWA)**   | Vitest 4.x + `jsdom` + `@testing-library/react` |
| **Config (API)**      | `apps/api/vitest.config.ts`                     |
| **Config (PWA)**      | `apps/pwa/vitest.config.ts`                     |
| **Quick run (API)**   | `pnpm --filter @familysync/api test`            |
| **Quick run (PWA)**   | `pnpm --filter @familysync/pwa test`            |
| **Full suite**        | `pnpm test` (from repo root — runs both apps)   |
| **Estimated runtime** | ~20-40 seconds (mocked DB + CalDAV; no network) |

---

## Sampling Rate

- **After every task commit:** Run the filtered quick command for the app touched
  (`pnpm --filter @familysync/api test -- <path>` or `pnpm --filter @familysync/pwa test -- <name>`).
- **After every plan wave:** Run `pnpm test` (full suite, both apps).
- **Before `/gsd-verify-work`:** Full suite must be green.
- **Max feedback latency:** ~40 seconds (full suite).

---

## Per-Task Verification Map

| Req ID       | Behavior                                                                                                    | Plan  | Wave | Test Type            | Automated Command                                            | File Exists   | Status     |
| ------------ | ----------------------------------------------------------------------------------------------------------- | ----- | ---- | -------------------- | ------------------------------------------------------------ | ------------- | ---------- |
| CAL-04       | `buildVeventString` → VCALENDAR for a timed event (DTSTART UTC)                                             | 02    | 2    | unit                 | `pnpm --filter @familysync/api test -- broker/vevent`        | ❌ W0 (03-01) | ⬜ pending |
| CAL-04       | `buildVeventString` → all-day event uses DATE not DATETIME (D-13)                                           | 02    | 2    | unit                 | same                                                         | ❌ W0 (03-01) | ⬜ pending |
| CAL-04       | POST /api/events/create → 202 + inserts pending outbox row                                                  | 03    | 2    | unit (mocked DB)     | `pnpm --filter @familysync/api test -- routes/events`        | ❌ W0 (03-01) | ⬜ pending |
| CAL-05       | PATCH /api/events/:uid/edit → 202 + inserts row with etag                                                   | 03    | 2    | unit                 | same                                                         | ❌ W0 (03-01) | ⬜ pending |
| CAL-06       | DELETE /api/events/:uid → 202 + inserts delete row                                                          | 03    | 2    | unit                 | same                                                         | ❌ W0 (03-01) | ⬜ pending |
| CAL-07       | `buildVeventString` with `rruleString` → RRULE property                                                     | 02    | 2    | unit                 | `pnpm --filter @familysync/api test -- broker/vevent`        | ❌ W0 (03-01) | ⬜ pending |
| CAL-04/05/06 | Outbox worker: pending→done (204), pending→failed (412), pending→backoff (500), pending→dead (max attempts) | 02/04 | 2    | unit                 | `pnpm --filter @familysync/api test -- broker/outboxWorker`  | ❌ W0 (03-01) | ⬜ pending |
| CAL-04/05/06 | GET /api/events/sync-status returns outbox status (D-09)                                                    | 03    | 2    | unit                 | `pnpm --filter @familysync/api test -- routes/events`        | ❌ W0 (03-01) | ⬜ pending |
| CAL-04/05/07 | GET /api/events/writable-calendars returns D-03 set; never another member's read-only personal (V4)         | 03    | 2    | unit                 | `pnpm --filter @familysync/api test -- routes/events`        | ❌ W0 (03-01) | ⬜ pending |
| D-08         | 412 → conflict (not retry), mark failed, trigger re-sync                                                    | 04    | 2    | unit                 | `pnpm --filter @familysync/api test -- broker/outboxWorker`  | ❌ W0 (03-01) | ⬜ pending |
| D-04         | Edit-as-move emits DELETE+CREATE pair; create runs first                                                    | 04    | 2    | unit                 | same                                                         | ❌ W0 (03-01) | ⬜ pending |
| D-03/V4      | create rejects write to non-owned/non-shared calendar (403)                                                 | 03    | 2    | unit                 | `pnpm --filter @familysync/api test -- routes/events`        | ❌ W0 (03-01) | ⬜ pending |
| PWA-01       | `vite.config.ts` produces valid `manifest.webmanifest` with required fields                                 | 06    | 3    | smoke (build output) | `pnpm --filter @familysync/pwa build` + manifest field check | ❌ W0 (03-01) | ⬜ pending |
| PWA-02       | `isIOSSafariNonStandalone()` true on mock iOS Safari non-standalone UA                                      | 06    | 3    | unit                 | `pnpm --filter @familysync/pwa test -- InstallPrompt`        | ❌ W0 (03-01) | ⬜ pending |
| PWA-02       | `useAndroidInstallPrompt` sets `canInstall=true` on `beforeinstallprompt`                                   | 06    | 3    | unit (mock event)    | same                                                         | ❌ W0 (03-01) | ⬜ pending |

_Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky_

---

## Wave 0 Requirements

The Wave 0 RED test scaffold is created in **plan 03-01 Task 4** (these files import
not-yet-existing modules so they fail RED until later waves implement them):

- [ ] `apps/api/tests/broker/vevent.test.ts` — CAL-04, CAL-07 (VEVENT builder, DATE/DATETIME split, RRULE)
- [ ] `apps/api/tests/broker/write.test.ts` — tsdav call shapes, etag/If-Match, response interpretation
- [ ] `apps/api/tests/broker/outboxWorker.test.ts` — outbox state machine (done/failed/backoff/dead), edit-as-move ordering (D-04/D-07/D-08)
- [ ] `apps/api/tests/routes/events.test.ts` — EXTEND existing: POST /create, PATCH /edit, DELETE /:uid, GET /sync-status, GET /writable-calendars, 403 ownership (preserve existing GET /api/events block)
- [ ] `apps/pwa/src/components/InstallPrompt.test.tsx` — iOS detection, Android `beforeinstallprompt` capture (PWA-02)

Existing test files (`broker/sync`, `routes/events` GET block, `auth/devBypass`) remain in place.

---

## Manual-Only Verifications

| Behavior                                                      | Requirement | Why Manual                                                          | Test Instructions                                                    |
| ------------------------------------------------------------- | ----------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
| SW `navigateFallbackDenylist` excludes `/callback`            | PWA-01      | Requires a real production build + SW registration over HTTPS       | Verify against prod build; confirm `/callback` not intercepted by SW |
| iOS standalone PWA login completes without leaving standalone | Gate 2      | Requires a physical iPhone, installed PWA, Authelia OIDC round-trip | Follow `docs/deployment.md` Gate 2 checklist (Plan 07)               |

---

## Validation Sign-Off

- [x] All tasks have `<automated>` verify or a Wave 0 RED dependency (created in 03-01 Task 4)
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references (five RED files in 03-01)
- [x] No watch-mode flags
- [x] Feedback latency < 40s
- [x] `nyquist_compliant: true` set in frontmatter

**Approval:** approved 2026-06-05
