Skip to content

test(web-e2e): close the last failures — 36/36 against a local Neon stack#2511

Merged
andrew-bierman merged 14 commits into
feat/web-e2e-fixfrom
test/playwright-web-pass
May 30, 2026
Merged

test(web-e2e): close the last failures — 36/36 against a local Neon stack#2511
andrew-bierman merged 14 commits into
feat/web-e2e-fixfrom
test/playwright-web-pass

Conversation

@andrew-bierman

@andrew-bierman andrew-bierman commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Brings the Playwright web suite from 0/36 (couldn't even start against this branch's setup) to 36/36 running headless on a fully local stack — and unblocks running it from wrangler dev against a local Postgres at all.

Sits on top of feat/web-e2e-fix so the diff is focused on what's new: local-dev infrastructure, two genuine product bugs surfaced by E2E, and a few test polish items.

What's in here (in commit order)

Local Neon stack for wrangler dev

  • 🐳 docker-compose.test.yml adds ghcr.io/timowilhelm/local-neon-http-proxy on port 4444 alongside the existing wsproxy. Lets @neondatabase/serverless (the HTTP driver Better Auth uses, plus the WS Pool) talk to the local pg without any driver/adapter switch. No code change to auth/index.ts's DB connection.
  • 🐛 Worker entry sets neonConfig.fetchEndpoint + wsProxy when NEON_DATABASE_URL host is db.localtest.me, and src/db/index.ts excludes that host from the standard-pg routing so every component (Better Auth + Eden Treaty + queue workers) stays on the same neon-http driver path as prod. Also adds CORS preflight handling for /api/auth/* — Better Auth's handler returns 404 on OPTIONS, so browser-based sign-in from a different origin never reached it.
  • 🔒 Better Auth trustedOrigins accepts http://localhost:* (gated on BETTER_AUTH_URL.startsWith('http://localhost')), so any local port works for parallel dev agents without an allowlist update.
  • 🔧 docker-compose.test.yml host ports are env-var overrideable (POSTGRES_TEST_HOST_PORT, WSPROXY_HOST_PORT, NEON_PROXY_HOST_PORT) — defaults unchanged. Lets two worktrees / two agents bring up the stack on the same box without colliding.

Seed fixes

  • 🌱 seed-e2e-user.ts now also inserts the account credential row. Better Auth's email/password flow reads the password from account.password, not users.password_hash. Migration 0042_migrate_auth_data copies users → account at migrate time — before the E2E user exists — so the seeded user was silently un-signable against a fresh local DB.
  • 🌱 New seed-e2e-catalog.ts — 10 hand-picked items spanning Sleep System / Shelter / Packs / Cooking / Lighting / Water / Apparel, embeddings via OpenAI text-embedding-3-small, idempotent on SKU. Production populates catalog_items via the ETL workflow; a fresh local DB has zero rows, which broke every catalog test.

Two real product bugs surfaced by the E2E pass

  • 🏷️ NativeWindUI LargeTitleHeadersearchBar.testID was set on the JSX prop object but never propagated to the rendered icon Button. Patched in patches/@packrat-ai%2Fnativewindui@2.0.3.patch so the testID lands on the actual DOM element. Worth a parallel upstream PR (see Follow-ups).
  • 🐛 Expo AI chat — the chat transport's Authorization header was captured from authClient.useSession() data at transport-build time. On first render that's null → the very first send went out as Bearer null → 401 → useChat showed the generic "something went wrong" banner. Switched to DefaultChatTransport's Resolvable<Record<string, string>> form: read the token from authClient.getSession() lazily at each send (it's cached on native via SecureStore, on web via Better Auth's session atom).

Test polish

  • ✅ Playwright config: channel: 'chrome' (Playwright's bundled chromium has no Ubuntu 26.04 build yet), --incognito+--no-default-browser-check+--no-first-run+--password-store=basic so the test browser never reads from the dev's real Chrome profile, fullyParallel: true, 4 workers (override via PW_WORKERS).
  • 🔒 Headless by default everywhere. Opt into a visible browser with PWHEADED=1. Previously local dev was headed by default which pops Chrome windows on the dev's desktop unexpectedly.
  • 🩹 settings screen loads regex tolerates the (Dev) build tag — production builds render PackRat v…, dev/preview render PackRat (Dev) v….
  • 🔧 New typed-env entries: OPENAI_API_KEY in nodeEnvSchema for the catalog seed; NEON_LOCAL_PROXY_PORT (optional) in apiEnvSchema so the local proxy port comes from the validated Worker env, not raw process.env.

Test plan

Verified locally:

  • bun test:expo — 326/326 pass
  • bun test:api:unit — 215/215 pass
  • bunx tsc --noEmit in apps/expo — no new errors (1 pre-existing uuid type def issue)
  • Playwright suite (headless, parallel × 4) — 36/36 in ~60s on bumped ports (18082 / 18787 / 14444 / 15433 / 15434) and on default ports
  • Sign-in works against local pg through neon-http-proxy (HTTP /sql endpoint)
  • No .maestro/** flow files touched; only runtime change in apps/expo is the chat transport, which uses the same authClient API on iOS/Android

Local quickstart

# 1. Bring up pg + wsproxy + neon-proxy
cd packages/api
docker compose -f docker-compose.test.yml up -d

# 2. Point env at the local stack
# (root .env.local)
NEON_DATABASE_URL=postgres://test_user:test_password@db.localtest.me:5433/packrat_test
NEON_DATABASE_URL_READONLY=postgres://test_user:test_password@db.localtest.me:5433/packrat_test
EXPO_PUBLIC_API_URL=http://localhost:8787

# 3. Migrate + seed
bun run db:migrate
NEON_DATABASE_URL=$NEON_DATABASE_URL E2E_TEST_EMAIL=… E2E_TEST_PASSWORD=… bun run db:seed:e2e-user
NEON_DATABASE_URL=$NEON_DATABASE_URL OPENAI_API_KEY=… bun run packages/api/src/db/seed-e2e-catalog.ts

# 4. Bring up API + Expo web
bun api                          # wrangler dev on :8787
bun --cwd apps/expo web          # expo on :8081

# 5. Run the suite
cd apps/expo
TEST_EMAIL=… TEST_PASSWORD=… bun test:web

For a parallel agent / second worktree, bump ports:

POSTGRES_TEST_HOST_PORT=15433 WSPROXY_HOST_PORT=15434 NEON_PROXY_HOST_PORT=14444 \
  COMPOSE_PROJECT_NAME=packrat-playwright \
  docker compose -f docker-compose.test.yml up -d
# Then point NEON_DATABASE_URL at :15433, set NEON_LOCAL_PROXY_PORT=14444,
# wrangler dev --port 18787, expo --port 18082.

Follow-ups (not in this PR)

  • Open the parallel upstream PR on PackRat-AI/nativewindui for the searchBar.testID propagation fix so the local patch can be deleted on next release.
  • The chat transport's useSession() call is left in place purely to keep the session-fetch on mount; consider removing it once the call-side migration is complete elsewhere.

Summary by CodeRabbit

  • New Features

    • Added E2E catalog seeding with AI-generated product embeddings.
    • Enhanced local development support for configurable database proxy.
  • Bug Fixes

    • Improved session initialization to prevent authentication race conditions.
  • Tests

    • Enabled parallel test execution with dynamic worker configuration.
    • Expanded version matching to support environment-tagged build strings.
  • Chores

    • Updated authentication CORS handling for cross-origin requests.
    • Added environment variable support for local development configuration.

Review Change Stack

ghcr.io/timowilhelm/local-neon-http-proxy lets the @neondatabase/serverless
HTTP and WS drivers talk to local Postgres on a single port. We add it
alongside the existing wsproxy so the Playwright E2E setup (wrangler dev
against db.localtest.me) shares the exact code paths Better Auth and Eden
Treaty use in prod — no driver/adapter switching.
…outes

Two pieces that together let `wrangler dev` work from the local web app:

1. `maybeConfigureLocalNeon` in the Worker entry flips
   `neonConfig.fetchEndpoint` (HTTP /sql) and `neonConfig.wsProxy` (WS /v2)
   at port 4444 when NEON_DATABASE_URL points at db.localtest.me. Combined
   with skipping the db.localtest.me host in src/db/index.ts'
   isStandardPostgresUrl check, every component (Better Auth + Eden Treaty
   + queue workers) routes through the same neon-http driver as prod.
   Eliminates the "Cannot perform I/O on behalf of a different request"
   hang from cross-request Pool WebSocket reuse.

2. CORS preflight handler for /api/auth/* — Better Auth's handler returns
   404 on OPTIONS, so browser-based sign-in from a different origin (Expo
   web on :8082, API on :8787) never reached Better Auth. We mirror the
   Elysia CORS allowlist (packrat.world subdomains, localhost, exp://)
   and append Access-Control headers to Better Auth's response too.
Better Auth's `trustedOrigins` previously only allowed the API base URL
itself (`http://localhost:8787`), so sign-in calls from the Expo web app
at `http://localhost:8082` were rejected by the CSRF check.

Gate the extra entries on `env.BETTER_AUTH_URL.startsWith('http://localhost')`
so the trust list never widens in production. (Can't use ENVIRONMENT here —
its Zod default is 'production' when unset, which would dead-code this.)
…n sign in the E2E user

Better Auth's email/password sign-in reads the password from
`account.password` (with `providerId='credential'`), not
`users.password_hash`. The 0042 data-migration copies users → account at
migrate time, before any user exists; the E2E user is created *after*
that migration runs, so it had no credential account row and sign-in
returned 401 against a fresh local DB.

This commit also captures the user id from the create branch so we don't
re-query for it before inserting the account row.
…nAI embeddings)

Production populates `catalog_items` via the ETL workflow scraping product
pages. A fresh local DB has zero rows, which breaks any test that scrolls
the catalog tab, runs a similarity search, or adds an item from catalog
to a pack.

Ten hand-picked items spanning Sleep System / Shelter / Packs / Cooking /
Lighting / Water / Apparel give the catalog tab something to render and
make "sleeping bag" a meaningful search query. Embeddings are generated
once via OpenAI text-embedding-3-small (matches the schema's 1536-dim
vector column). Idempotent on SKU — safe to re-run.

Usage:
  NEON_DATABASE_URL=... OPENAI_API_KEY=... \
    bun run packages/api/src/db/seed-e2e-catalog.ts
…wright config

- `channel: 'chrome'` so the suite runs against the system Chrome — Playwright's
  bundled chromium has no Ubuntu 26.04 build yet, so installing the default
  browser would fail on newer Linux dev boxes.
- `--incognito` / `--no-default-browser-check` / `--no-first-run` /
  `--password-store=basic` make the browser explicitly ephemeral so it never
  reads from the developer's personal Chrome profile.
- Headless follows env: `CI=true` or `PWHEADLESS=1` → headless; otherwise
  headed locally so you can watch the run.
- `fullyParallel: true` and 4 workers (override with `PW_WORKERS`) — tests
  use timestamped names so parallel runs don't collide.
- globalSetup uses the same chrome channel + headless toggle so its initial
  sign-in matches the test browser environment.
…for session

- The settings screen renders `PackRat (Dev) v2.0.26` in dev/preview builds
  but `PackRat v2.0.26` in production. Widen the regex to accept an
  optional `(...)` environment marker so the same assertion works on every
  build variant.
- The AI chat transport's Authorization header is sourced from
  `authClient.useSession()`, which returns null on first render and only
  populates after the first `/api/auth/get-session` response resolves and
  React commits the new state. The test was clicking Send before that
  cycle finished, sending `Bearer null` → 401. Wait for the session
  network call AND a 500ms React-settle before clicking.
…ton in LargeTitleHeader

Extends the existing patch. `LargeTitleHeader` accepts a `searchBar.testID`
prop and the comment in CatalogItemsScreen explicitly notes "testID exists
in runtime 2.0.5 implementation but absent from published types" — except
the published 2.0.3 implementation also doesn't forward it to the actual
icon Button on Android/web. The catalog-search Playwright test waits for
`getByTestId('catalog:search-btn')` to be visible and times out because the
testID is set on a JSX object that's never propagated to the DOM.

Adds `testID={props.searchBar.testID}` to the search-icon Button so the
testID lands on the rendered element. No-op on iOS (separate platform file)
and prod parity since the underlying NativeWindUI 2.0.5 has this fix too.
…n't Bearer null

`useSession()` returns `data: null` on first render and only populates after
the first GET /api/auth/get-session resolves AND React commits the new
state. The chat transport captured `token` in its memoized closure, so the
very first send went out as `Authorization: Bearer null` and the API
responded 401 — useChat then showed the generic "something went wrong"
banner and the test timed out waiting for an item response.

`DefaultChatTransport.headers` accepts `Resolvable<Record<string, string>>`,
which means a function/promise. Read the token from
`authClient.getSession()` lazily at each send: getSession is cached after
the first call so this is cheap, and we never capture `null` in the
closure. Removes the matching playwright wait-for-session band-aid from
core.spec.ts since the race no longer exists.
…is trusted

Replaces the fixed list (8081/8082/3000/19006) with a single wildcard so
parallel dev agents on bumped ports (e.g. 18082) don't need a one-by-one
allowlist update. Better Auth's trustedOrigins supports wildcard patterns
via the internal wildcardMatch helper.

Still gated on `env.BETTER_AUTH_URL.startsWith('http://localhost')` so prod
never widens trust.
Each host port in docker-compose.test.yml now reads from an env var with
the original value as the default — POSTGRES_TEST_HOST_PORT,
WSPROXY_HOST_PORT, NEON_PROXY_HOST_PORT.

Lets multiple dev agents (or two worktrees) on the same machine spin up
the test stack on different host ports without modifying the compose file
or colliding with each other. Default behaviour is unchanged.

Usage:
  POSTGRES_TEST_HOST_PORT=15433 WSPROXY_HOST_PORT=15434 \
    NEON_PROXY_HOST_PORT=14444 COMPOSE_PROJECT_NAME=foo \
    docker compose -f docker-compose.test.yml up -d
…EADED=1

Previously the local default was headed so the developer could watch the
run — but that pops Chrome windows on the desktop unexpectedly when an
agent or background script triggers the suite. Invert the default: the
suite is headless unless `PWHEADED=1` is explicitly set. Both
playwright.config.ts and globalSetup.ts now share this gate.
…XY_PORT

The repo's clean-checks pre-push hook flags raw `process.env.X` reads in
non-bootstrap code. Three fixes:

- Add `OPENAI_API_KEY` to nodeEnvSchema so seed-e2e-catalog.ts reads it
  via `nodeEnv.OPENAI_API_KEY` like every other Node/Bun script.
- Add `NEON_LOCAL_PROXY_PORT` (optional) to the Worker's apiEnvSchema so
  the local-dev neon proxy port comes from the validated env binding,
  not from a raw `process.env` read inside the Worker entry.
- Thread the proxy port from `e.NEON_LOCAL_PROXY_PORT` into
  `maybeConfigureLocalNeon` as an explicit argument.
Copilot AI review requested due to automatic review settings May 28, 2026 05:48
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file api mobile database labels May 28, 2026
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@andrew-bierman, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86e848be-a75c-4af6-86aa-f4fc6a76eafe

📥 Commits

Reviewing files that changed from the base of the PR and between 96c3783 and eb4a368.

📒 Files selected for processing (1)
  • apps/expo/app/(app)/ai-chat.tsx

Walkthrough

This PR establishes E2E testing infrastructure with local Neon proxy support and refactors AI chat transport authentication. Changes span mobile (chat auth), API (Worker routing, proxy config, seeding), database (URL classification, migrations), and Playwright test configuration.

Changes

AI Chat Transport Session Auth

Layer / File(s) Summary
Per-request session retrieval
apps/expo/app/(app)/ai-chat.tsx
AIChat calls authClient.useSession() early, and DefaultChatTransport builds auth headers asynchronously per request via authClient.getSession(), eliminating the render-time token race condition.

E2E Testing Infrastructure Setup

Layer / File(s) Summary
Neon proxy routing and auth CORS
packages/api/src/index.ts, packages/api/src/db/index.ts, packages/api/src/auth/index.ts
API Worker detects db.localtest.me and configures neonConfig HTTP/WS endpoints. /api/auth/** routes add CORS header mirroring with OPTIONS preflight handling. Auth config adds http://localhost:* wildcard. isStandardPostgresUrl excludes local proxy host.
Environment variables and Docker composition
packages/api/src/utils/env-validation.ts, packages/env/src/node.ts, packages/api/docker-compose.test.yml
Added NEON_LOCAL_PROXY_PORT, OPENAI_API_KEY env vars. Docker Compose parameterizes Postgres and proxy ports; new neon-proxy service (ghcr.io/timowilhelm/local-neon-http-proxy) on port 4444.
E2E catalog seeding with embeddings
packages/api/src/db/seed-e2e-catalog.ts
New script seeds fixed gear catalog with OpenAI embeddings (text-embedding-3-small), detects standard Postgres vs Neon HTTP, generates vectors for missing SKUs, idempotently inserts rows with embedding and optional brand.
E2E user credential seeding
packages/api/src/db/seed-e2e-user.ts
Tracks userId on user refresh, adds idempotent credential account upsert in Better Auth account table by userId + providerId. Validates user insert success and standardizes logging.
Playwright parallel execution and Chrome options
apps/expo/playwright/playwright.config.ts, apps/expo/playwright/tests/globalSetup.ts
Config enables fullyParallel, dynamic workers from PW_WORKERS, environment-driven headless mode. Chromium project uses local Chrome channel with incognito and startup flags. Global setup applies same launch configuration.
Test assertions and UI testID
apps/expo/playwright/tests/core.spec.ts, patches/@packrat-ai%2Fnativewindui@2.0.3.patch
Settings screen version assertion accepts optional environment tag (e.g., PackRat (Dev) v2.x). UI patch adds testID from props.searchBar.testID and // @ts-nocheck`` directives.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PackRat-AI/PackRat#2432: Updates chat transport initialization; overlaps on DefaultChatTransport auth-header refactoring and CustomChatTransport wiring in same file.
  • PackRat-AI/PackRat#2373: Refactors AI chat auth from stored token state to Better Auth session retrieval (useSession/getSession) with identical token-race motivation.

Suggested labels

web, api, database, mobile

Suggested reviewers

  • mikib0
  • Isthisanmol
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main objective: achieving 36/36 passing Playwright tests against a local Neon stack by closing remaining failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/playwright-web-pass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Expo Unit Tests Coverage (./apps/expo)

Status Category Percentage Covered / Total
🔵 Lines 82.61% 480 / 581
🔵 Statements 82.61% (🎯 75%) 480 / 581
🔵 Functions 92.59% 50 / 54
🔵 Branches 90.9% 170 / 187
File CoverageNo changed files found.
Generated in workflow #1482 for commit eb4a368 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for API Unit Tests Coverage (./packages/api)

Status Category Percentage Covered / Total
🔵 Lines 76.17% 502 / 659
🔵 Statements 76.17% (🎯 65%) 502 / 659
🔵 Functions 95% 38 / 40
🔵 Branches 88.67% 227 / 256
File CoverageNo changed files found.
Generated in workflow #1482 for commit eb4a368 by the Vitest Coverage Report Action

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR stabilizes and unblocks the Expo web Playwright E2E suite against a fully local stack by adding local Neon proxy infrastructure, seeding required DB data, and fixing a couple of product issues surfaced by E2E (Better Auth CORS preflight + AI chat auth header timing).

Changes:

  • Add a local Neon HTTP+WS proxy path for wrangler dev, plus explicit CORS preflight handling for /api/auth/*.
  • Add/adjust DB seed scripts for E2E (credential account row + a small catalog dataset with embeddings).
  • Update Playwright configuration to run headless by default and safely in parallel; fix AI chat transport to resolve auth headers per-request.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
patches/@PackRat-AI%2Fnativewindui@2.0.3.patch Patch NativeWindUI LargeTitleHeader to propagate testID to the rendered button.
packages/env/src/node.ts Add OPENAI_API_KEY to node/bun env schema for the new catalog seed script.
packages/api/src/utils/env-validation.ts Add validated NEON_LOCAL_PROXY_PORT for worker local-neon proxy routing.
packages/api/src/index.ts Configure Neon driver routing for local proxy + add /api/auth/* OPTIONS/CORS handling and CORS header propagation.
packages/api/src/db/seed-e2e-user.ts Ensure seeded E2E user is sign-in-able by inserting/updating the Better Auth account credential row.
packages/api/src/db/seed-e2e-catalog.ts New catalog seed script that generates embeddings and inserts a small, idempotent dataset.
packages/api/src/db/index.ts Exclude db.localtest.me from “standard Postgres” routing so Workers stay on Neon driver paths in local dev.
packages/api/src/auth/index.ts Expand Better Auth trustedOrigins to allow any http://localhost:* when running locally.
packages/api/docker-compose.test.yml Add Neon local HTTP+WS proxy service and allow host-port overrides for parallel stacks.
bun.lock Update dependency range for @packrat-ai/nativewindui (but currently inconsistent with overrides/patched deps).
apps/expo/playwright/tests/globalSetup.ts Launch Chrome headless by default and add hardening flags to avoid using real Chrome profiles.
apps/expo/playwright/tests/core.spec.ts Relax settings version text assertion to tolerate “(Dev)” build tags.
apps/expo/playwright/playwright.config.ts Enable safe parallelism, configurable workers, default headless, and use installed Chrome channel.
apps/expo/app/(app)/ai-chat.tsx Fix first-message 401 by resolving the Authorization header lazily via authClient.getSession().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +51
neon-proxy:
image: ghcr.io/timowilhelm/local-neon-http-proxy:main
environment:
PG_CONNECTION_STRING: postgres://test_user:test_password@postgres-test:5432/packrat_test
ports:
- "${NEON_PROXY_HOST_PORT:-4444}:4444"
Comment thread packages/api/src/index.ts
Comment on lines +130 to +145
// Better Auth does not implement CORS preflight (OPTIONS) responses, so
// we mirror the Elysia CORS allowlist here. Without this, browser-based
// sign-in calls from the web app (a different origin than the API) fail
// the preflight and never reach Better Auth.
const origin = request.headers.get('Origin');
const isAllowedOrigin =
!!origin &&
[
/^https:\/\/(www\.)?packrat\.world$/,
/^https:\/\/[\w-]+\.packrat\.world$/,
/^https:\/\/[\w-]+\.packratai\.com$/,
/^https?:\/\/[\w-]+\.workers\.dev$/,
/^http:\/\/localhost:\d+$/,
/^exp:\/\//,
].some((re) => re.test(origin));

Comment on lines +26 to +36
const isStandardPostgresUrl = (url: string) => {
try {
const u = new URL(url);
const host = u.hostname.toLowerCase();
const isNeonTech = host === 'neon.tech' || host.endsWith('.neon.tech');
const isNeonCom = host === 'neon.com' || host.endsWith('.neon.com');
return u.protocol === 'postgres:' && !isNeonTech && !isNeonCom;
} catch {
return false;
}
};

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
patches/@PackRat-AI%2Fnativewindui@2.0.3.patch (1)

6-6: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid // @ts-nocheck`` in patched TS/TSX sources.

patches/@packrat-ai%2Fnativewindui@2.0.3.patch adds // @ts-nocheck at lines 6, 15, and 24, disabling all type checking for the affected files. Remove it and address the actual type errors; if suppression is unavoidable, use a targeted `// `@ts-expect-error on the specific failing line with a brief rationale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/`@packrat-ai%2Fnativewindui@2.0.3.patch at line 6, Remove the global
suppression added by the token "// `@ts-nocheck`" in the patch file (present at
the top of the patched source) and instead fix the underlying TypeScript errors:
run the TypeScript build/tests to surface the compile errors, correct the
offending types/usages, and only where a specific line cannot be typed safely
add a narrowly scoped "// `@ts-expect-error` <short rationale>" immediately above
that single failing statement. Target the exact "// `@ts-nocheck`" occurrences
introduced by the patch and replace them with either proper type fixes or
line-level "// `@ts-expect-error`" annotations with a brief rationale so type
checking remains enabled elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/expo/app/`(app)/ai-chat.tsx:
- Around line 142-146: The headers async function using authClient.getSession
returns a union where the empty-object branch doesn't satisfy
Record<string,string>; fix by ensuring both branches return a
Record<string,string> for the headers in headers (the token variable comes from
data?.session?.token). Update the return so the empty branch is typed as a
Record<string,string> (e.g., return an explicitly typed empty record) or add an
explicit return type for headers that ensures {} is treated as
Record<string,string>, and keep the Authorization branch returning a
Record<string,string> with `Bearer ${token}`.

In `@apps/expo/playwright/tests/globalSetup.ts`:
- Around line 19-23: The setup creates a Playwright browser via chromium.launch
but doesn’t guarantee browser.close() runs if subsequent setup steps throw; wrap
the code that uses the returned browser in a try/finally (use the existing
browser variable from chromium.launch) and call await browser.close() in the
finally block so the browser is always closed, rethrow or propagate any caught
error after cleanup; apply the same try/finally pattern around any other setup
sequence that opens the browser and relies on browser.close() (the block
currently calling browser.close()).

In `@packages/api/src/db/seed-e2e-catalog.ts`:
- Around line 26-36: Extract the duplicated isStandardPostgresUrl function into
a shared helper module (e.g., create a new exported function
isStandardPostgresUrl in a db utils module) and replace the local
implementations in seed-e2e-catalog.ts and seed-e2e-user.ts (and the similar
logic in db/index.ts) with imports from that new module; ensure the new module
exports the function, update the import statements in the three files to use the
shared utility, and run a quick build/TS check to confirm no type or import
errors.

In `@packages/api/src/index.ts`:
- Around line 130-180: Extract the hard-coded regex array used to compute
isAllowedOrigin into a shared constant named CORS_ORIGIN_PATTERNS and import/use
it wherever CORS is configured (replace the inline array used to build
isAllowedOrigin in the auth handler and the Elysia CORS config), then compute
isAllowedOrigin via CORS_ORIGIN_PATTERNS.some(re => re.test(origin)) so both the
auth handler (code around request.headers.get('Origin') / isAllowedOrigin /
corsHeaders) and the main Elysia CORS setup reference the same source of truth.

In `@packages/env/src/node.ts`:
- Around line 76-77: The OPENAI_API_KEY env schema currently uses
z.string().min(1).optional(), which doesn't enforce the required sk- prefix;
update the validation for OPENAI_API_KEY to require the OpenAI secret prefix
(e.g. use z.string().min(1).startsWith('sk-') or an equivalent regex) so the
node schema matches the API schema's behavior and fails fast; modify the
OPENAI_API_KEY schema entry accordingly (preserving .optional() if intended).

---

Outside diff comments:
In `@patches/`@packrat-ai%2Fnativewindui@2.0.3.patch:
- Line 6: Remove the global suppression added by the token "// `@ts-nocheck`" in
the patch file (present at the top of the patched source) and instead fix the
underlying TypeScript errors: run the TypeScript build/tests to surface the
compile errors, correct the offending types/usages, and only where a specific
line cannot be typed safely add a narrowly scoped "// `@ts-expect-error` <short
rationale>" immediately above that single failing statement. Target the exact
"// `@ts-nocheck`" occurrences introduced by the patch and replace them with
either proper type fixes or line-level "// `@ts-expect-error`" annotations with a
brief rationale so type checking remains enabled elsewhere.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c5bb9e07-c422-42bb-be7a-96f3c83f6211

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd42f9 and 96c3783.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock, !bun.lock
📒 Files selected for processing (13)
  • apps/expo/app/(app)/ai-chat.tsx
  • apps/expo/playwright/playwright.config.ts
  • apps/expo/playwright/tests/core.spec.ts
  • apps/expo/playwright/tests/globalSetup.ts
  • packages/api/docker-compose.test.yml
  • packages/api/src/auth/index.ts
  • packages/api/src/db/index.ts
  • packages/api/src/db/seed-e2e-catalog.ts
  • packages/api/src/db/seed-e2e-user.ts
  • packages/api/src/index.ts
  • packages/api/src/utils/env-validation.ts
  • packages/env/src/node.ts
  • patches/@packrat-ai%2Fnativewindui@2.0.3.patch

Comment thread apps/expo/app/(app)/ai-chat.tsx
Comment on lines +19 to +23
const browser = await chromium.launch({
channel: 'chrome',
headless: process.env.PWHEADED !== '1',
args: ['--incognito', '--no-default-browser-check', '--no-first-run', '--password-store=basic'],
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure browser cleanup on setup failures.

If any step throws after chromium.launch (Line 19), browser.close() (Line 65) is skipped. Wrap setup steps in try/finally so the browser always closes.

Proposed fix
-  const browser = await chromium.launch({
+  const browser = await chromium.launch({
     channel: 'chrome',
     headless: process.env.PWHEADED !== '1',
     args: ['--incognito', '--no-default-browser-check', '--no-first-run', '--password-store=basic'],
   });
-  const context = await browser.newContext();
-  const page = await context.newPage();
+  try {
+    const context = await browser.newContext();
+    const page = await context.newPage();
 
-  // Start from the auth entry screen, then click through to login
-  await page.goto(`${BASE_URL}/auth`, { waitUntil: 'load' });
+    // Start from the auth entry screen, then click through to login
+    await page.goto(`${BASE_URL}/auth`, { waitUntil: 'load' });
 
-  // ... existing setup steps ...
+    // ... existing setup steps ...
 
-  await context.storageState({ path: AUTH_STATE_PATH });
-  console.log(`[globalSetup] Logged in as ${email}`);
-
-  await browser.close();
+    await context.storageState({ path: AUTH_STATE_PATH });
+    console.log(`[globalSetup] Logged in as ${email}`);
+  } finally {
+    await browser.close();
+  }

Also applies to: 65-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/playwright/tests/globalSetup.ts` around lines 19 - 23, The setup
creates a Playwright browser via chromium.launch but doesn’t guarantee
browser.close() runs if subsequent setup steps throw; wrap the code that uses
the returned browser in a try/finally (use the existing browser variable from
chromium.launch) and call await browser.close() in the finally block so the
browser is always closed, rethrow or propagate any caught error after cleanup;
apply the same try/finally pattern around any other setup sequence that opens
the browser and relies on browser.close() (the block currently calling
browser.close()).

Comment on lines +26 to +36
const isStandardPostgresUrl = (url: string) => {
try {
const u = new URL(url);
const host = u.hostname.toLowerCase();
const isNeonTech = host === 'neon.tech' || host.endsWith('.neon.tech');
const isNeonCom = host === 'neon.com' || host.endsWith('.neon.com');
return u.protocol === 'postgres:' && !isNeonTech && !isNeonCom;
} catch {
return false;
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Extract isStandardPostgresUrl to a shared utility.

This helper is duplicated verbatim in seed-e2e-user.ts and has similar logic in packages/api/src/db/index.ts. Extract to a common module (e.g., packages/api/src/db/utils.ts) to keep the seed scripts DRY.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/db/seed-e2e-catalog.ts` around lines 26 - 36, Extract the
duplicated isStandardPostgresUrl function into a shared helper module (e.g.,
create a new exported function isStandardPostgresUrl in a db utils module) and
replace the local implementations in seed-e2e-catalog.ts and seed-e2e-user.ts
(and the similar logic in db/index.ts) with imports from that new module; ensure
the new module exports the function, update the import statements in the three
files to use the shared utility, and run a quick build/TS check to confirm no
type or import errors.

Comment thread packages/api/src/index.ts
Comment on lines +130 to +180
// Better Auth does not implement CORS preflight (OPTIONS) responses, so
// we mirror the Elysia CORS allowlist here. Without this, browser-based
// sign-in calls from the web app (a different origin than the API) fail
// the preflight and never reach Better Auth.
const origin = request.headers.get('Origin');
const isAllowedOrigin =
!!origin &&
[
/^https:\/\/(www\.)?packrat\.world$/,
/^https:\/\/[\w-]+\.packrat\.world$/,
/^https:\/\/[\w-]+\.packratai\.com$/,
/^https?:\/\/[\w-]+\.workers\.dev$/,
/^http:\/\/localhost:\d+$/,
/^exp:\/\//,
].some((re) => re.test(origin));

const corsHeaders: Record<string, string> = isAllowedOrigin
? {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
Vary: 'Origin',
}
: {};

if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers':
request.headers.get('Access-Control-Request-Headers') ??
'Content-Type, Authorization, X-API-Key',
'Access-Control-Max-Age': '86400',
},
});
}

const validatedEnv = getEnv();
const auth = await getAuth(validatedEnv);
return auth.handler(request);
const authResponse = await auth.handler(request);
if (!isAllowedOrigin) return authResponse;
// Copy Better Auth's response and append CORS headers so cookies/JSON
// payloads reach the cross-origin caller.
const headers = new Headers(authResponse.headers);
for (const [k, v] of Object.entries(corsHeaders)) headers.set(k, v);
return new Response(authResponse.body, {
status: authResponse.status,
statusText: authResponse.statusText,
headers,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

CORS allowlists diverge between Elysia and auth handler.

The auth handler adds patterns (packratai.com, workers.dev) not present in the Elysia CORS config (lines 63-69). If these origins need auth access, they likely need access to other API routes too. This duplication will drift over time.

Extract the allowlist to a shared constant.

♻️ Proposed fix
+const CORS_ORIGIN_PATTERNS = [
+  /^https:\/\/(www\.)?packrat\.world$/,
+  /^https:\/\/[\w-]+\.packrat\.world$/,
+  /^https:\/\/[\w-]+\.packratai\.com$/,
+  /^https?:\/\/[\w-]+\.workers\.dev$/,
+  /^http:\/\/localhost:\d+$/,
+  /^exp:\/\//,
+];
+
 export const app = new Elysia({ adapter: CloudflareAdapter })
   .use(
     cors({
       credentials: true,
       origin: (request) => {
         const origin = request.headers.get('Origin');
         if (!origin) return false;
-        const allowed = [
-          /^https:\/\/(www\.)?packrat\.world$/,
-          /^https:\/\/[\w-]+\.packrat\.world$/,
-          /^http:\/\/localhost:\d+$/,
-          /^exp:\/\//,
-        ];
-        return allowed.some((re) => re.test(origin));
+        return CORS_ORIGIN_PATTERNS.some((re) => re.test(origin));
       },

Then use CORS_ORIGIN_PATTERNS in the auth handler too (line 137).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/index.ts` around lines 130 - 180, Extract the hard-coded
regex array used to compute isAllowedOrigin into a shared constant named
CORS_ORIGIN_PATTERNS and import/use it wherever CORS is configured (replace the
inline array used to build isAllowedOrigin in the auth handler and the Elysia
CORS config), then compute isAllowedOrigin via CORS_ORIGIN_PATTERNS.some(re =>
re.test(origin)) so both the auth handler (code around
request.headers.get('Origin') / isAllowedOrigin / corsHeaders) and the main
Elysia CORS setup reference the same source of truth.

Comment thread packages/env/src/node.ts
Comment on lines +76 to +77
// ── OpenAI (packages/api/src/db/seed-e2e-catalog.ts) ──────────────
OPENAI_API_KEY: z.string().min(1).optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider adding sk- prefix validation for consistency.

The API env schema (packages/api/src/utils/env-validation.ts:49) validates OPENAI_API_KEY with .startsWith('sk-'). This node schema only checks min(1). Misconfigurations will fail later at the OpenAI API rather than at parse time.

♻️ Suggested change
-  OPENAI_API_KEY: z.string().min(1).optional(),
+  OPENAI_API_KEY: z.string().startsWith('sk-').optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ── OpenAI (packages/api/src/db/seed-e2e-catalog.ts) ──────────────
OPENAI_API_KEY: z.string().min(1).optional(),
// ── OpenAI (packages/api/src/db/seed-e2e-catalog.ts) ──────────────
OPENAI_API_KEY: z.string().startsWith('sk-').optional(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/env/src/node.ts` around lines 76 - 77, The OPENAI_API_KEY env schema
currently uses z.string().min(1).optional(), which doesn't enforce the required
sk- prefix; update the validation for OPENAI_API_KEY to require the OpenAI
secret prefix (e.g. use z.string().min(1).startsWith('sk-') or an equivalent
regex) so the node schema matches the API schema's behavior and fails fast;
modify the OPENAI_API_KEY schema entry accordingly (preserving .optional() if
intended).

… string>

CI's tsc caught a union-vs-index-signature issue from the previous commit's
conditional return (`token ? {Authorization: …} : {}`) — the empty-object
branch isn't assignable to `Record<string, string>` because TypeScript
can't infer an index signature from a literal `{}`.

Build the object explicitly so the inferred return is a single concrete
`Record<string, string>`.
@andrew-bierman andrew-bierman merged commit cacb3f7 into feat/web-e2e-fix May 30, 2026
7 checks passed
@andrew-bierman andrew-bierman deleted the test/playwright-web-pass branch May 30, 2026 07:27
andrew-bierman added a commit that referenced this pull request Jun 1, 2026
…al patches

Upstream `@packrat-ai/nativewindui@2.0.6` ships everything our local patch
was working around:

- LargeTitleHeader: `testID={props.searchBar.testID}` on the search-icon
  Button (the catalog-search testID fix we added in PR #2511).
- Type-only fixes from the v2.0.5 type-issue cleanup
  (`chore: fix type issues` + `🏷️ fix all TypeScript errors after fresh
  component sync`), which made the `@ts-nocheck` hunks on
  AdaptiveSearchHeader / Icon/types / LargeTitleHeader obsolete.

Updates:
- `packages/ui/package.json` was already at `^2.0.6`; bump the root
  `overrides`, root direct dep, and the locked version in bun.lock so
  installs actually pick up 2.0.6 instead of being pinned to 2.0.3.
- Delete `patches/@PackRat-AI%2Fnativewindui@2.0.3.patch` and remove the
  `patchedDependencies` entry — no patches needed against 2.0.6.

Verified:
- `bun install` resolves to 2.0.6 cleanly.
- `tsc --noEmit` clean against `apps/expo` (only the pre-existing `uuid`
  type-def error remains, unrelated).
- Playwright web E2E suite is 36/36 green against the bumped version on
  the same local Neon stack from PR #2511.
andrew-bierman added a commit that referenced this pull request Jun 1, 2026
…al patches

Upstream `@packrat-ai/nativewindui@2.0.6` ships everything our local patch
was working around:

- LargeTitleHeader: `testID={props.searchBar.testID}` on the search-icon
  Button (the catalog-search testID fix we added in PR #2511).
- Type-only fixes from the v2.0.5 type-issue cleanup
  (`chore: fix type issues` + `🏷️ fix all TypeScript errors after fresh
  component sync`), which made the `@ts-nocheck` hunks on
  AdaptiveSearchHeader / Icon/types / LargeTitleHeader obsolete.

Updates:
- `packages/ui/package.json` was already at `^2.0.6`; bump the root
  `overrides`, root direct dep, and the locked version in bun.lock so
  installs actually pick up 2.0.6 instead of being pinned to 2.0.3.
- Delete `patches/@PackRat-AI%2Fnativewindui@2.0.3.patch` and remove the
  `patchedDependencies` entry — no patches needed against 2.0.6.

Verified:
- `bun install` resolves to 2.0.6 cleanly.
- `tsc --noEmit` clean against `apps/expo` (only the pre-existing `uuid`
  type-def error remains, unrelated).
- Playwright web E2E suite is 36/36 green against the bumped version on
  the same local Neon stack from PR #2511.
andrew-bierman added a commit that referenced this pull request Jun 1, 2026
…cal patches

Upstream `@packrat-ai/nativewindui@2.0.6` ships everything the local patch
was working around:

- LargeTitleHeader: `testID={props.searchBar.testID}` on the search-icon
  Button (the catalog-search testID fix we added in PR #2511).
- Type fixes from `chore: fix type issues` + `🏷️ fix all TypeScript errors
  after fresh component sync` that made the local `@ts-nocheck` hunks on
  AdaptiveSearchHeader / Icon/types / LargeTitleHeader obsolete.

Changes:
- `packages/ui/package.json` is already the canonical declarer at `^2.0.6`.
- **Drop the root `overrides` pin** for nativewindui. It was added in
  `8a558939e` to freeze us at 2.0.3 because 2.0.6 had a type regression
  against react-native 0.81 autocapitalize types. We're on RN 0.83.6 now,
  the regression no longer reproduces, and overrides should be reserved
  for emergency pins, not normal version management. With the override
  gone, the `^2.0.6` range in `packages/ui` is the single source of
  truth — future releases bump naturally without touching the root.
- Empty `patchedDependencies` block and delete the patch file — nothing
  to patch against 2.0.6.

Verified:
- `bun install` resolves to 2.0.6 cleanly with no root override.
- `bunx tsc --noEmit` clean on `apps/expo` (only the pre-existing `uuid`
  type-def error remains, unrelated).
- Playwright web E2E suite is 36/36 green against 2.0.6 with no patches.
andrew-bierman added a commit that referenced this pull request Jun 1, 2026
Resolution strategy (per fly-on-the-wall rule — development's bug fixes win
for everything except the e2e test infrastructure itself):

* App code conflicts (ai-chat.tsx, _layout.tsx, one-time-password.tsx,
  UserAvatar.tsx, pack/trip components, hooks, trips route): take theirs.
* packages/api/src/db/seed-e2e-user.ts: take theirs — uses accountId=email
  (better-auth's actual lookup key) and proper .onConflictDoUpdate().
* .github/workflows/web-e2e-tests.yml: take theirs — adds e2e-gate job,
  smart path filters, and a 'seed E2E user' step.
* apps/expo/playwright/{playwright.config.ts,tests/*.ts}: take ours.
  Development's parallel attempt still uses retired /api/auth/login etc;
  ours is the working 36/36 Better Auth suite from #2511.
* Root package.json: keep our #2528 cleanup — drop the
  '@packrat-ai/nativewindui: 2.0.3-2' override block, keep
  'patchedDependencies: {}'.
* packages/ui/package.json: take ours (^2.0.6).
* bun.lock: regenerated via 'bun install'.
* packages/api/src/db/seed-e2e-catalog.ts: switch import from local
  './schema' to '@packrat/db/schema' — development moved the schema
  into the @packrat/db workspace.

Verified:
* bun test:expo — 357/357
* bun test:api:unit — 328/328
* bun check-types — 0 errors
* @packrat-ai/nativewindui resolves to 2.0.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api database dependencies Pull requests that update a dependency file mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants