# E2E test environment — a whole second copy of the app, on its own throwaway # database, with its own throwaway Redis. # # docker compose -f docker-compose.yml -f docker-compose.e2e.yml up -d pediatric-scribe-e2e # docker compose -f docker-compose.yml -f docker-compose.e2e.yml down -v postgres-e2e redis-e2e pediatric-scribe-e2e # # Normally you want scripts/e2e.sh, which does both around a test run. # # It used to share production's Postgres — same server, same database, same # table. Seeded robots sat in `users` next to real clinicians, and anything a # test wrote, or a migration under test changed, landed on real data. Nothing # about "run the tests" should be able to reach an account belonging to a # person. Now the stack has a database of its own, held in a tmpfs: it exists # in RAM, it is created empty on every `up`, and it is gone on `down`. The # schema is rebuilt each time by the container's own migrations, which also # means every run proves the migrations still work from nothing. services: # ── Throwaway Postgres ──────────────────────────────────────────────── # Same pinned image as production, so an e2e pass says something about what # production will do. PGDATA points at a subdirectory because initdb wants a # 0700 directory of its own and a tmpfs mountpoint is not one. postgres-e2e: image: pgvector/pgvector:pg16@sha256:00ba258a66dac104fd5171074a0084462a64a1369d8513f3d0a634e2f24d15bc container_name: pedscribe-db-e2e environment: POSTGRES_DB: pedscribe_e2e POSTGRES_USER: pedscribe POSTGRES_PASSWORD: e2e-throwaway PGDATA: /var/lib/postgresql/data/pgdata tmpfs: # In RAM, so there is no volume to forget to clean up and nothing to # survive a reboot. 1G is far more than a seeded test run uses. - /var/lib/postgresql/data:size=1g healthcheck: test: ["CMD-SHELL", "pg_isready -U pedscribe -d pedscribe_e2e"] interval: 3s timeout: 5s retries: 20 restart: "no" # ── Throwaway Redis ─────────────────────────────────────────────────── # Sessions and rate-limit counters. Persistence off in both directions: no # RDB snapshots, no AOF, and /data on tmpfs, so a run cannot inherit state # from the one before it. redis-e2e: image: redis:8-alpine@sha256:d146f83b1e0f02fc27c26a50cee39338c736674c5959db84363e6ae3cd9e02d2 container_name: ped-ai-redis-e2e command: ["redis-server", "--save", "", "--appendonly", "no"] tmpfs: - /data:size=64m healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 3s timeout: 5s retries: 20 restart: "no" # ── The app under test ──────────────────────────────────────────────── pediatric-scribe-e2e: build: context: . args: GIT_REVISION: ${GIT_REVISION:-unknown} image: ped-ai-e2e:latest ports: - "127.0.0.1:3553:3000" networks: # Its own project network, plus the converter so PDF export is exercised # here too. Without this the e2e stack could only reach Postgres and # Redis, and a PDF download failed in a way production would not. - default - danvics_convert env_file: - .env environment: # These four are the isolation. The entrypoint applies OpenBao secrets # only for keys docker has not already set, so anything named here wins # over the vault — which is exactly what that rule was written for. DATABASE_URL: postgresql://pedscribe:e2e-throwaway@postgres-e2e:5432/pedscribe_e2e REDIS_URL: redis://redis-e2e:6379 # Never mail a real person from a test run. SMTP_HOST: "" # Disable Turnstile entirely — both server-side verification AND the # client-side widget. Without clearing the SITE_KEY the frontend tries # to initialise the Turnstile iframe against the prod domain and # throws error 110200, which Playwright's pageerror guard correctly # flags as an uncaught exception. TURNSTILE_SECRET_KEY: "" TURNSTILE_SITE_KEY: "" # A key of its own. Rows here are throwaway, and binding them to the # production key would be the one piece of production that leaked in. DATA_ENCRYPTION_KEY: "e2e0000000000000000000000000000000000000000000000000000000000e2e" # Raise the login rate-limit so Playwright multi-worker runs don't # trip the production 10/15min cap. Only affects this e2e container. LOGIN_RATE_LIMIT_MAX: "500" # Also raise the global /api/ limit so multi-spec Playwright runs # that make hundreds of API calls don't burn through the 200/min cap. API_RATE_LIMIT_MAX: "5000" # Allow fetches from the two origins Playwright serves tests from — # the in-network hostname and the host-port loopback. Without this # the CORS middleware (scoped to /api) rejects any non-GET request # because .env's APP_URL points at the production domain. CORS_ORIGINS: "http://pediatric-ai-scribe-e2e:3000,http://host.docker.internal:3553,http://localhost:3553,http://127.0.0.1:3553" volumes: - scribe-logs-e2e:/app/data/logs depends_on: postgres-e2e: condition: service_healthy redis-e2e: condition: service_healthy container_name: pediatric-ai-scribe-e2e # Not unless-stopped: this is a test rig, not a service. It should not come # back on its own after a reboot, and it should not outlive a `down`. restart: "no" healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"] interval: 5s timeout: 5s retries: 12 start_period: 15s # ── The last run's report ───────────────────────────────────────────── # Playwright writes a self-contained HTML report; this serves it so there is # a link to open rather than a directory to find. Traces and screenshots of # failures are in there, which is the part worth looking at on a phone. e2e-report: image: nginx:alpine container_name: pediatric-ai-scribe-e2e-report ports: - "127.0.0.1:3554:80" volumes: - ./e2e/playwright-report:/usr/share/nginx/html:ro restart: "no" volumes: scribe-logs-e2e: networks: danvics_convert: external: true