# Zync OS QA Round 1 — executable journey contract

Slug: `zync-os-qa-round1`
Date: 2026-07-13
Audience: implementers, reviewers, CI maintainers

## Rationale

The desktop and mobile OS plans landed with green model/synthetic checks while real mounted UI journeys remained broken. This change makes the canonical user journeys executable, fail-closed, and release-blocking across classic and OS presentation modes.

Hardening rationale: static readiness must reject the current partial 32-case branch before Playwright discovery can wait on a preview/build slot, while the merged suite remains an exact 38-case × four-surface gate.

## Authority and supersession

- `docs/user_journeys/00-index.md` is authoritative for journey IDs. It currently defines 38 `UJ-*` entries.
- Every canonical journey executes on exactly four surfaces: `legacy-desktop`, `legacy-mobile`, `os-desktop`, and `os-mobile`. Required matrix size: 152.
- The mobile OS spec supersedes the desktop spec's earlier “classic below 768px” rule. A capable mobile viewport may mount `MobileOsShell`; `?shell=classic` remains the explicit legacy override.
- Synthetic `page.setContent()`, pure store/model tests, screenshots of fabricated markup, route interception, soft assertions, conditional skips, and test-only production hooks do not count as journey coverage.
- Existing unit/model tests remain useful below the journey gate.

## Current-state baseline

- Canonical coverage is 0/152.
- The landed OS suite contains 57 test declarations, two explicit mobile skips, and substantial synthetic/model coverage.
- PR CI runs the default and recovery Playwright configs but not the OS config or a journey config.
- The OS Playwright web server times out after 180 seconds during a production build and emits a conflicting color-environment warning.
- Mobile root wiring leaves Recents and notifications as no-ops, routes Today to `/`, persists no remote mobile mutations, and exposes only six OS-owned modules.
- Desktop source now contains right snap, eight resize handles, Start/Settings/Today wiring, a bounded command list, and root overflow containment, but tests do not mount and operate those paths.
- Residual desktop defects include viewport/taskbar geometry disagreement, no blank-desktop context menu, generic systray actions, unsafe PDF response acceptance, stale Start-menu test ownership, and broad layout-network suppression.
- Anonymous 401 responses from authenticated APIs are correct. Authenticated 500s are not.
- `/api/customers/new` falls through the UUID detail route and can produce a database 500. It must be rejected as a non-UUID API path; `/customers/new` remains the UI route.
- Shell mode and `OsShell` independently fetch layout. Failed mutations are retried in a way that can create request storms.
- Current focus/control borders fail the 3:1 non-text contrast floor; dark `ink-faint` fails 4.5:1 on hover surfaces.

## Journey harness contract

### Registry

`tests/journeys/registry.ts` owns one executable `JourneyDefinition` per canonical ID. Each definition contains:

- exact ID, title, source document, actor, fixture requirements, and semantic steps;
- no surface-specific selectors;
- explicit expected status transitions and allowed negative-path responses;
- cleanup contract.

`registry-guard.spec.ts` parses the journey index and fails when an ID is missing, duplicated, extra, source-less, skipped, or not expanded over all four surfaces.

### Static matrix readiness

- `scripts/check-journey-matrix.mjs` MUST find exactly 38 `tests/journeys/cases/**/UJ-###.spec.ts` files, with one file for every canonical ID, before invoking Playwright discovery.
- Discovery MUST use a non-navigation base URL and a bounded 30-second child timeout. Timeout, spawn, JSON, and non-zero child failures MUST report the cause and exit non-zero.
- The checker MUST require exactly four configured projects in this order: `legacy-desktop`, `legacy-mobile`, `os-desktop`, `os-mobile`, and exactly 152 normal executions.

### Runtime environment contract

- `tests/journeys/runtime-env.ts` owns the typed required-variable schema for normal journey execution.
- The schema requires existing values for `DEMO_EMAIL`, `DEMO_PASSWORD`, `PORTAL_TENANT_SLUG`, `PORTAL_EMAIL`, `PORTAL_PASSWORD`, `CONTRACTOR_PORTAL_TOKEN`, `ADMIN_BASE_URL`, `APP_BASE_URL`, `STATUS_BASE_URL`, `ADMIN_JOURNEY_TENANT_SLUG`, `ADMIN_JOURNEY_TENANT_USER_EMAIL`, `ADMIN_EMAIL`, `ADMIN_PASSWORD`, `ADMIN_TOTP_SECRET`, `SUPPORT_ADMIN_EMAIL`, `SUPPORT_ADMIN_PASSWORD`, and `SUPPORT_ADMIN_TOTP_SECRET`.
- The journey fixture validates all requirements before navigation and reports every missing variable. It MUST NOT invent values or silently default required credentials, IDs, tokens, or endpoints.
- `tsconfig.qa.json` MUST typecheck every `tests/journeys/**/*.ts` and `tests/qa/**/*.ts` source file; the normal app typecheck remains scoped to application source.

### Surface adapters

`SurfaceAdapter` is the only presentation seam. It owns:

- viewport and shell override;
- authentication entry;
- direct legacy route opening;
- OS Start/app-drawer launch, window/frame readiness, mobile home/back/recents behavior;
- route/deep-link equivalence;
- stable role/name/test-id selectors.

Journey code requests semantic actions such as `openModule`, `openRecord`, `submitForm`, and `expectOutcome`. It never branches on viewport or shell internals.

### Actors and data

- Worker-scoped, deterministic tenant/run IDs isolate parallel execution.
- Fixtures create real sessions and real data through supported API/service contracts. No raw SQL in Playwright.
- The six shared cases provision their owner actors through `POST /api/admin/tenants/provision`; tenant, user, customer, invitation, and link identifiers are discovered through supported APIs during each run, never fixed in environment variables.
- UJ-001 creates a unique mailbox and signup address per execution and retrieves that execution's verification link; no single-use verification URL or token is shared between projects.
- Required actors include tenant roles, invited users, contractor, customer portal, accountant, signatory, and system-admin principals.
- External routes still execute in every surface project. The surface controls viewport and entry context; public/portal routes remain outside authenticated shell chrome as their specs require.
- Cleanup is idempotent and bounded. Failed cleanup is a test failure with an attached artifact.

## Fail-closed diagnostics

Each test installs diagnostics before navigation and emits one JSON artifact per execution.

It records and fails on:

- uncaught page errors;
- console errors and warnings;
- same-origin request failures and unexpected non-2xx responses;
- router warnings, hydration errors, CSP failures, broken images/fonts, and unhandled dialogs;
- visible controls without an accessible name, unreachable keyboard controls, clipped dialogs, document overflow, and broken links;
- axe violations and token-pair contrast violations;
- missing screenshots, traces, or diagnostic output when the test fails.

Noise exceptions are exact records keyed by origin, method, path, status, and lifecycle phase. Every exception has owner, rationale, and expiry. Blanket 401, “Failed to fetch”, layout, or console suppression is forbidden. A 401 is allowed only in a journey step that explicitly expects anonymous/expired authentication.

## OS behavior acceptance

### Desktop

- Start, Settings, Today, every taskbar item, and every systray item perform distinct tested actions.
- Snap works at left and right edges. All eight resize handles honor minimum size, viewport bounds, RTL, and taskbar inset.
- Desktop and document never scroll; app content scrolls inside the window.
- Blank desktop and icon context menus use the shared primitive, support pointer and keyboard invocation, and close with Escape.
- Command center footer is visible, result content scrolls, input height is compact, and focus returns to the invoker.
- Window position/size/snap/icon/pin changes survive reload and remain tenant/user/device isolated.
- Statement PDF responses require PDF content type, `%PDF` magic, EOF, and parser-safe bytes before download.

### Mobile

- Home seeds role-filtered defaults when remote/local layout is absent, invalid, or legacy-empty. Explicit reset may intentionally persist an empty layout.
- Today launches `/dashboard`.
- All journey-owned modules are reachable through manifest-backed app drawer/home metadata.
- Search, notifications, notification deep links, mark-read, Back, Home, Recents, close/close-all guards, and app launch are wired.
- Mobile layout mutations persist remotely with the same version-conflict contract as desktop.
- Touch targets, gestures, safe areas, keyboard, RTL/LTR, WebKit/Chromium, PWA offline/update, and reduced motion are tested on the mounted app.

## API and database acceptance

- Route registration tests exercise the assembled Worker, not isolated handlers only.
- `/api/time/active` and shell layout GET/PUT/DELETE are registered and authenticated.
- Invalid customer IDs return a deterministic 400/404 and never reach a UUID query.
- Shell layout reads are deduplicated. Writes are debounced, latest-only, single-flight, bounded, conflict-aware, and never retry 401/403/validation failures.
- Shell layout queries preserve tenant, user, device, and version isolation and audit successful mutations transactionally.
- `device_class` has a database constraint for `desktop|mobile`; migration journal and replay gates remain clean.

## Design-system acceptance

The existing identity remains dark navy/teal/indigo. Only failing semantic pairs change:

- dark `ink-faint`: `oklch(61% .025 215)`;
- light `control-border`: `oklch(62% .03 195)`;
- dark `control-border`: `oklch(49% .045 230)`;
- light `focus-ring`: `oklch(62% .08 195)`;
- dark `focus-ring`: `oklch(49% .08 195)`.

`on-accent`, `accent-fg`, and `accent-foreground` resolve to `ink-on-accent`. Text token and background token combinations are typed/registered; `ink` on accent backgrounds is invalid.

`/design-system` registers every exported primitive and relevant normal, hover, focus, active, disabled, loading, error, empty, selected, checked, open, compact, and reduced-motion state. It includes OS window/taskbar/Start/tray/notification states.

The catalogue is mounted outside the authenticated shell so production builds expose a stable, session-independent QA surface; this keeps visual and accessibility evidence deterministic without changing shell or journey behavior.

Visual gate: `1440x900` and `390x844` × light/dark × LTR/RTL, axe, computed contrast, keyboard operation, focus trap/return, and no skipped states.

The visual gate uses one worker-scoped browser context/page per project and one initial `/design-system` navigation; its three assertions reuse that stable project session. The Vite API proxy matches `/api/` only. Root-level built chunks whose names begin `api-` MUST load as static assets and MUST NOT be sent to the API proxy. Full-page baselines remain the authoritative complete visual comparison; section segmentation requires measured instability and equivalent complete coverage.

## Wallpaper assets

- Desktop: `public/wallpapers/zync-space-desktop.avif`, 3840×2160.
- Mobile: `public/wallpapers/zync-space-mobile.avif`, 2160×3840.
- Both use coordinated original space artwork, centered translucent Z watermark, `cover center`, and quiet icon/taskbar/dock zones.

## CI gate

- Exactly 38 canonical journey case files, exactly four journey projects, and 152 discovered executions.
- The dedicated journey/diagnostics typecheck and exact matrix checker run before expensive journey shards.
- One typed runtime-environment manifest covers every credential, endpoint, and stable fixture reference used by all 38 cases; the shard workflow declares the same names and validation reports the complete missing set before navigation.
- The shared gate contains exactly six canonical cases and 24 listings (six cases × four projects), with one shared case file per journey rather than per-surface copies.
- Shared CI configuration is minimal and fail-closed: `E2E_ADMIN_EMAIL`, `E2E_ADMIN_PASSWORD`, and `E2E_ADMIN_TOTP_SECRET` perform the real admin login plus TOTP verification for supported provisioning; `E2E_MAILBOX_API_KEY` creates per-run mailboxes for link retrieval; and `E2E_FIREBASE_API_KEY` plus a dedicated phone actor's `E2E_FIREBASE_REFRESH_TOKEN` exchange for a fresh Firebase ID token on every run. Missing configuration fails before navigation. The admin session and Firebase ID token are never fixed CI inputs. The mailbox credential remains scoped to disposable inboxes, while the refresh token is the only long-lived Firebase credential and can be revoked independently.

Rationale (2026-07-13): service-account custom-token minting was rejected because no service-account material or custom-token exchange exists in this repository, and introducing a project-level private key would grant broader privilege than a dedicated phone actor's revocable refresh token. Direct phone-auth automation was rejected because it requires SMS/reCAPTCHA interaction and has no supported headless test contract. Firebase's refresh-token exchange is the supported stable-credential flow and preserves the server verifier's required `phone_number` claim.
- Eight deterministic shards, two workers per shard unless measured capacity requires less.
- Zero skips, fixmes, soft-only assertions, or flaky outcomes.
- Retry may collect infrastructure diagnostics once, but a first-attempt failure still marks the gate flaky/failing.
- Reports, JSON diagnostics, traces, videos, screenshots, and failure DOM snapshots upload with `if: always()`.

### Auth-email delivery contract

Auth routes enqueue typed `auth.email` jobs and surface queue failures to the caller. The registered consumer renders the supported notification templates and sends through the platform mail adapter backed by Resend. It acknowledges only after a successful provider call, uses `auth-email:<message-id>` as the provider idempotency key, and retries failed deliveries with bounded backoff without acknowledging them. Resend and the repository's Cloudflare bindings provide outbound delivery but no mailbox retrieval API; MailSlurp is therefore the minimal external CI dependency for end-to-end link retrieval, and its API key is the only mailbox credential required. The namespaced discriminator keeps the auth consumer separate from existing customer-communications messages that use `type: 'email'`.

Rationale (2026-07-13): the previous shared journeys reused one signup identity and fixed verification URL across four projects, while the auth producer's `email` jobs had no registered consumer and were acknowledged as unhandled. Per-run supported provisioning/mailboxes and durable queue delivery make the shared cases runnable without fixture IDs or test-only auth paths.

## Definition of done

All targeted unit/API/query/type/build checks pass cleanly; all 152 journey executions pass; OS interaction, visual, accessibility, diagnostics, and persistence gates pass; no warning is ignored; specs match code; branch `os-qa-round1` is pushed and a PR is open.
