diff --git a/.github/workflows/api-e2e.yml b/.github/workflows/api-e2e.yml index 5f074555..fd5cea76 100644 --- a/.github/workflows/api-e2e.yml +++ b/.github/workflows/api-e2e.yml @@ -23,6 +23,7 @@ env: PGLITE: true DATABASE_URL: postgresql://localhost/test JWT_SECRET: e2e-jwt-secret-min-32-chars-for-tests + AI_PROVIDER: anthropic ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} jobs: @@ -53,6 +54,16 @@ jobs: run: pnpm turbo run test --filter=@repo/api - name: Install Playwright run: pnpm --filter @repo/api exec playwright install chromium --with-deps + - name: Verify ANTHROPIC_API_KEY for E2E (AI_PROVIDER=anthropic) + shell: bash + env: + AI_PROVIDER: ${{ env.AI_PROVIDER }} + ANTHROPIC_API_KEY: ${{ env.ANTHROPIC_API_KEY }} + run: | + if [[ "${AI_PROVIDER}" == "anthropic" && -z "${ANTHROPIC_API_KEY}" ]]; then + echo "::error::AI_PROVIDER is anthropic but ANTHROPIC_API_KEY is empty or unset. Set the ANTHROPIC_API_KEY repository secret; required before E2E tests (local)." >&2 + exit 1 + fi - name: E2E tests (local) run: pnpm --filter @repo/api test:e2e:local - name: Upload Playwright report diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml index e0a64982..42428d34 100644 --- a/.github/workflows/web-e2e.yml +++ b/.github/workflows/web-e2e.yml @@ -27,6 +27,7 @@ env: DATABASE_URL: postgresql://localhost/test JWT_SECRET: e2e-jwt-secret-min-32-chars-for-tests NEXT_PUBLIC_API_URL: http://localhost:3001 + AI_PROVIDER: anthropic ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} jobs: @@ -57,6 +58,16 @@ jobs: run: pnpm turbo run test --filter=@repo/web - name: Install Playwright run: pnpm --filter @repo/web exec playwright install chromium --with-deps + - name: Verify ANTHROPIC_API_KEY for E2E (AI_PROVIDER=anthropic) + shell: bash + env: + AI_PROVIDER: ${{ env.AI_PROVIDER }} + ANTHROPIC_API_KEY: ${{ env.ANTHROPIC_API_KEY }} + run: | + if [[ "${AI_PROVIDER}" == "anthropic" && -z "${ANTHROPIC_API_KEY}" ]]; then + echo "::error::AI_PROVIDER is anthropic but ANTHROPIC_API_KEY is empty or unset. Set the ANTHROPIC_API_KEY repository secret; required before E2E tests (local)." >&2 + exit 1 + fi - name: E2E tests (local) env: SKIP_BUILD: '1' diff --git a/apps/api/.env.test.example b/apps/api/.env.test.example index 5372e24d..4dcf7937 100644 --- a/apps/api/.env.test.example +++ b/apps/api/.env.test.example @@ -28,6 +28,7 @@ DATABASE_URL=postgresql://localhost/test # AI # ----------------------------------------------------------------------------- ANTHROPIC_API_KEY=sk-ant-xxx +AI_PROVIDER=anthropic # OPEN_ROUTER_API_KEY= # OLLAMA_BASE_URL=http://localhost:11434 # AI_DEFAULT_MODEL=claude-sonnet-4-20250514 diff --git a/apps/api/scripts/run-e2e-local.mjs b/apps/api/scripts/run-e2e-local.mjs index 63486a2b..84c71e74 100644 --- a/apps/api/scripts/run-e2e-local.mjs +++ b/apps/api/scripts/run-e2e-local.mjs @@ -2,6 +2,7 @@ /** * E2E local: spawn Fastify API, poll until healthy, run Playwright, cleanup on exit. * No wait-on. Uses ALLOW_TEST, PGLITE, NODE_ENV=test. + * Forces AI_PROVIDER=anthropic and strips Open Router/Ollama/AI_DEFAULT_MODEL (parity with Vitest + web E2E). */ import { spawn, spawnSync } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' @@ -67,6 +68,13 @@ async function main() { ) process.exit(1) } + const anthropicApiKey = loaded.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY + if (!anthropicApiKey) { + process.stderr.write( + 'E2E local: ANTHROPIC_API_KEY must be set in .env.test or process.env when AI_PROVIDER is anthropic. Refusing to run without it.\n', + ) + process.exit(1) + } const env = { ...process.env, ...loaded, @@ -74,7 +82,12 @@ async function main() { PGLITE: 'true', NODE_ENV: 'test', JWT_SECRET: jwtSecret, + AI_PROVIDER: 'anthropic', + ANTHROPIC_API_KEY: anthropicApiKey, } + delete env.OPEN_ROUTER_API_KEY + delete env.OLLAMA_BASE_URL + delete env.AI_DEFAULT_MODEL const fastify = spawn(process.execPath, ['--import', 'tsx', 'server.ts'], { cwd: fastifyDir, diff --git a/apps/docu/content/docs/testing/frontend-testing.mdx b/apps/docu/content/docs/testing/frontend-testing.mdx index 4732ddd3..2c6cbfb8 100644 --- a/apps/docu/content/docs/testing/frontend-testing.mdx +++ b/apps/docu/content/docs/testing/frontend-testing.mdx @@ -58,7 +58,7 @@ test.describe('Magic Link Auth E2E', () => { - **workers: 1** — Required for PGlite (Fastify test DB) to avoid concurrent writer issues - **Test order** — Auth project first (magic-link-auth: error tests, login/logout; ends with logout). Chromium: unauth badge tests first, then first authed test triggers fixture and confirms session, then remaining authed tests run consecutively -- **Chat E2E** — Requires `OPEN_ROUTER_API_KEY`; see [E2E Testing](/docs/testing/e2e-testing) for env vars. If UNAUTHORIZED, session/Bearer propagation to Fastify `/ai/chat` needs debugging. +- **Chat E2E** — Requires `ANTHROPIC_API_KEY` (Anthropic Sonnet); see [E2E Testing](/docs/testing/e2e-testing) for env vars. If UNAUTHORIZED, session/Bearer propagation to Fastify `/ai/chat` needs debugging. - **Session model** — One login per worker via `authenticatedStorageState` fixture; cookies/storage saved to `test-results/.auth/user.json` (gitignored); each authed test gets a new context created with that storageState. No per-test login - **Login/logout tests** — Import from `@playwright/test`, use `page`; need fresh contexts to verify the flow - **Authed feature tests** — Import from `e2e/fixtures`, use `authenticatedPage`; no `loginAsTestUser` in test body. Each test gets a fresh context with saved storageState; navigate to target route (e.g. `goto('/')`) before asserting diff --git a/apps/web/e2e/chat-assistant.spec.ts b/apps/web/e2e/chat-assistant.spec.ts index d1b1a207..b4f580bf 100644 --- a/apps/web/e2e/chat-assistant.spec.ts +++ b/apps/web/e2e/chat-assistant.spec.ts @@ -32,7 +32,7 @@ test.describe('Chat Assistant', () => { if ( /insufficient_quota|insufficient_credits|quota_exceeded|credits_exceeded/i.test(errorText) ) { - test.skip(true, 'OpenRouter 402 insufficient credits') + test.skip(true, 'AI provider quota/credits (402)') return } if (/invalid x-api-key|authentication_error|invalid.*api.*key|401/i.test(errorText)) { diff --git a/apps/web/scripts/start-e2e-servers.mjs b/apps/web/scripts/start-e2e-servers.mjs index 9e795ec6..935da3f2 100644 --- a/apps/web/scripts/start-e2e-servers.mjs +++ b/apps/web/scripts/start-e2e-servers.mjs @@ -4,6 +4,7 @@ * Run in one terminal, then in another: pnpm test:e2e * * Use when Playwright's webServer spawns processes that get OOM killed (exit 137) on constrained VMs. + * Pins AI to Anthropic for API (same as test:e2e:local). */ import { spawn } from 'node:child_process' import { dirname } from 'node:path' @@ -18,7 +19,17 @@ const env = { PGLITE: 'true', NODE_ENV: 'test', NEXT_PUBLIC_API_URL: 'http://localhost:3001', + AI_PROVIDER: 'anthropic', } +if (env.AI_PROVIDER === 'anthropic' && !String(env.ANTHROPIC_API_KEY ?? '').trim()) { + process.stderr.write( + 'start-e2e-servers: ANTHROPIC_API_KEY is required when AI_PROVIDER is anthropic. Set it in the environment (e.g. .env.local) before starting E2E servers.\n', + ) + process.exit(1) +} +delete env.OPEN_ROUTER_API_KEY +delete env.OLLAMA_BASE_URL +delete env.AI_DEFAULT_MODEL const api = spawn('pnpm', ['--filter', '@repo/api', 'start:ci'], { cwd: repoRoot,