Passing OpenRouter's metadata filters does not prove a model can complete a turn through this harness, so dead-but-advertised models kept reaching users and failing mid-stream. A new model_compatibility table holds one verdict per model, a weekly Celery sweep re-probes stale entries, and catalogue generation reads the blocked ids back so those models are never offered. Also excludes :batch variants, which advertise full chat metadata and then reject completions outright. Auto-pin now falls through to Auto when an explicitly selected global config is absent from the current worker's catalogue. Returning it handed load_llm_bundle an id it could not resolve, ending the turn with SERVER_ERROR on one of the four uvicorn workers while the identical request succeeded on the others. Catalogue refresh also re-materializes GLOBAL_MODELS, which previously kept its boot snapshot until restart. On the web side a single dropped connection surfaced as one toast per in-flight query and dozens of PostHog exceptions, with no retry button. Network errors now share a toast id, are deduped per endpoint before capture, carry host and online context, and count as retryable. Consolidates the model filter helpers duplicated between the model list and OpenRouter services into openrouter_model_normalizer, renames OoxmlDefect to OoxmlError, and includes a ruff format and import-order pass. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| .. | ||
| connectors | ||
| documents/file-upload | ||
| fixtures | ||
| helpers | ||
| smoke | ||
| unit | ||
| auth.setup.ts | ||
| README.md | ||
Playwright E2E Suite
End-to-end tests for the full SurfSense stack (Next.js + FastAPI + Celery + Postgres + Redis). Designed to scale from one connector (Composio Drive in Phase 1) to every connector + manual file upload without rewriting the harness.
How the deterministic harness works
There are three layers of defense against accidental real-world calls. None of them touch production code.
surfsense_backend/tests/e2e/run_backend.pyandrun_celery.pyare separate entrypoints (not used bypython main.py). They hijacksys.modules["composio"]BEFORE importing the app, swap in strict fakes forlangchain_litellm/langchain_openai, and mount theX-E2E-Scenariomiddleware.- The fakes themselves are strict: every class implements
__getattr__that raisesNotImplementedErroron unknown surface. Adding a new SDK call site without updating the fake fails CI loudly. - CI sets
HTTPS_PROXY=http://127.0.0.1:1plus sentinel API keys (COMPOSIO_API_KEY=e2e-deny-real-call-sentinel). Any leaked outbound HTTP call fails before reaching the network.
Running locally
The recommended flow runs only Postgres and Redis in Docker, and the backend
- Celery worker on the host. The E2E entrypoints
setdefaultevery backend variable they need, so no.envfile is required on a fresh checkout.
One-time setup
From surfsense_web/:
pnpm install
pnpm exec playwright install --with-deps chromium
Each run
1. Bring up Postgres + Redis from the repo root:
docker compose -f docker/docker-compose.deps-only.yml up -d db redis
2. Start the backend in surfsense_backend/, terminal A:
uv sync
uv run alembic upgrade head
uv run python tests/e2e/run_backend.py
3. Start the Celery worker in surfsense_backend/, terminal B:
uv run python tests/e2e/run_celery.py
4. Register the Playwright user:
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"e2e-test@surfsense.net","password":"E2eTestPassword123!"}'
5. Run Playwright from surfsense_web/, terminal C:
pnpm test:e2e # dev server (fast iteration)
pnpm test:e2e:headed # show the browser
pnpm test:e2e:ui # Playwright UI mode
pnpm test:e2e:debug # Playwright Inspector
pnpm test:e2e:prod # build + start (matches CI exactly)
pnpm test:e2e:report # open the last HTML report
playwright.config.ts and the backend run scripts share defaults, so the
above works without exporting any env vars. Override
PLAYWRIGHT_TEST_EMAIL, PLAYWRIGHT_TEST_PASSWORD, or
NEXT_PUBLIC_FASTAPI_BACKEND_URL only when pointing tests at a different
stack.
To debug a single journey:
pnpm test:e2e:headed connectors/composio/drive/journey.spec.ts
Hermetic alternative (matches CI)
To reproduce the CI environment exactly: backend and Celery in containers with L3 egress denied, replace steps 1–3 with:
docker compose -f docker/docker-compose.e2e.yml up -d --build --wait
Then run steps 4 (curl register) and 5 (pnpm test:e2e:prod) as above. Tear
down with:
docker compose -f docker/docker-compose.e2e.yml down -v --remove-orphans
This builds the ~9 GB e2e backend image, so the deps-only flow is faster for day-to-day work.
Adding a new connector
The directory tree is designed so a new connector lives mostly inside its own folder. E2E is scoped to one user expectation per connector: the smallest browser journey that proves the user-visible outcome works. Follow this checklist:
- Backend fake. Add a new file under
surfsense_backend/tests/e2e/fakes/<sdk>_module.pymirroringcomposio_module.py. Use__getattr__to raise on unknown surface. - Hijack. Wire the new module into
run_backend.pyandrun_celery.pywithsys.modules["<sdk>"] = <fake>. - Backend tests. Put edge cases in backend tests, not Playwright:
OAuth state validation in unit tests, and route/error branches in
surfsense_backend/tests/integration/<connector>/. - Fixtures. Drop a fixture file into
tests/fixtures/connectors/that returns a pre-connected connector row. - Journey spec. Create exactly one
tests/connectors/<vendor>/<service>/journey.spec.tsfor the user expectation. For indexable connectors this usually means connect -> select scope -> index -> assert canary content. For connection-only connectors this means connect -> assert connected badge. - Update this README's directory diagram.
Do not add separate Playwright specs for expired OAuth state, duplicate
connectors, auth-expired classification, or route config persistence.
Those belong in backend unit/integration tests such as
surfsense_backend/tests/unit/utils/test_oauth_security.py and
surfsense_backend/tests/integration/composio/.
Why API-driven?
Journey specs prefer a thin browser assertion followed by API-driven configuration/indexing because:
- It keeps tests deterministic (no waiting on UI animation, React hydration, or Next.js compile time).
- It exercises the same backend code path the UI eventually calls.
- The expensive E2E assertion stays focused on what only E2E can prove: the cross-process seam from connector -> Celery -> indexing -> DB.
UI-only tests live under helpers/ui/ for future Phase 2 work
(folder-tree drag-and-drop, indexing options switches, etc.).