1. What already exists (don’t rebuild)
.github/workflows/e2e-full.yml— full Playwright E2E on the DGX self-hosted runner (ARM64), opt-in per-PR via thee2e-fulllabel + scheduled. Default PRs run onlytest:e2e:smoke(fast).- An autoheal loop is already wired into that workflow: on E2E failure it computes regressions (
scripts/specs/find-regressions.ts), then runs theencore-test-triageskill per regressed test (≤5), gated by anAUTO_HEAL_ENABLED=1kill-switch and ahealer-skipPR label; scheduled runs file a healer-candidates issue. Emitsplaywright-report/healer-events.jsonl. - The
browser-autoheal/heal-e2eskills — the dev-loop counterpart (drive in Playwright, capture console/pageerror/network≥400, classify, fix-to-green; never auto-merge). - Playwright bundled chromium works in the runner (
npx playwright install chromium). - A local Supabase stack (
encoreosDocker project) + the ability to serve edge functions withPROLIANT_FIXTURE_TRANSPORT=1(proven this session).
2. The three blockers (why it isn’t trustworthy yet)
G1 — Keystone: the app can’t be pointed at a test database
vite.config.ts resolveStaticPublicEnv() hardcodes VITE_SUPABASE_URL from a Vercel-project-ID → dev2/prod2 map and ignores any VITE_SUPABASE_URL env override. The define: block bakes that URL into the bundle. Consequences:
.env.local(local stack) is ignored by every build and the dev server.- e2e-full.yml passes
E2E_VITE_SUPABASE_URLas a build env — but it’s a no-op; with noVERCEL_PROJECT_IDon the runner,resolveSupabaseTarget()falls through todev, so CI E2E silently runs against shared dev2. (Confirm by inspecting a built bundle in CI; fix regardless.)
resolveStaticPublicEnv() honor an explicit override — when isE2E or a Supabase-branch URL is injected (Vercel preview), use the provided VITE_SUPABASE_URL/key; otherwise the project-ID map. Hard production-safety guard: a non-prod URL must be impossible to bake into a production/Vercel-prod build — extend tests/integration/environment-isolation.test.ts + scripts/utils/check-no-prod-refs to assert both directions. This single change unblocks local-hermetic, the (currently dead) E2E secret, and Supabase branching.
G2 — Environment model: shared dev2 vs per-PR isolation
Today E2E hits dev2 — shared, mutable, not the PR’s code. That makes autoheal flaky (concurrent state) and unable to test pre-merge branch changes (the F1/F2/F3 fixes wouldn’t be live there). Options in §3.G3 — Seed coverage: flows can’t reach their terminal screen
config.toml [db.seed] loads only base org/users/sites/schedules; module seeds (04_hr–12) are excluded for “schema/enum drift.” So the Proliant wizard (needs active integration + calendar + run-state) and WENO prescribe (needs org creds) have no data to drive. E2E currently papers over this with page.route mocks — which is exactly why it never caught that the real backend was broken.
Fix: a maintained seed:e2e path (resolve the enum drift, or a dedicated E2E seed module) that plants the per-flow fixtures, runnable against whichever target DB §2 chooses. Reuse the integration-test seeders (seedProliantStage2Fixtures, etc.).
G4 — Vendor-call determinism (+ F4a auth seed, already fixed)
Proliant/WENO make external calls. The test DB’s served functions must either run withPROLIANT_FIXTURE_TRANSPORT=1 (deterministic, recommended) or hold sandbox creds (needs BAA). Decision needed per env. (Auth-seed NULL-token 500 — F4a — is fixed in supabase/seeds/base/02_users.sql.)
3. Recommended environment model — Supabase preview branch per PR
You raised this, and it’s the right primary target. A Supabase preview branch is an ephemeral project created per Git branch, with the branch’s migrations applied + seed run + functions deployed. Wire the Vercel preview deploy to the matching branch and autoheal drives the preview URL. Why it wins for “ongoing”:- Hermetic + per-PR — no shared-state flakiness (fixes dev2’s core problem).
- Tests the branch’s own code — migrations + edge functions + seed are the PR’s (F1/F2/F3 would be live and catchable).
- Cloud parity — same runtime as prod, no local-stack management.
- G1 keystone — the preview build/app must target the branch URL, not dev2. Supabase’s Vercel integration injects the branch URL; vite.config must honor it (today it won’t).
- Enable branching — Supabase dashboard + GitHub integration + a
[branching]block; pick persistent vs ephemeral, and a cost/quota guardrail (branches cost money + spin-up latency ~minutes → per-PR, not per-commit). - G3 seed on branch creation; G4 fixture-transport as branch function env.
- PHI guardrail — preview branches must seed only synthetic data (no prod PHI), enforced.
4. Interactive MCP — recommendation: no, not for the pipeline
You asked whether to use the interactive Playwright/cursor-ide-browser MCP. Recommendation: automated Playwright (headless chromium) + the existing healer is the ongoing pipeline; interactive MCP is a debugging aide only.- Automated runner — deterministic, gateable, CI-native, already wired (e2e-full + healer). This is what “ongoing” requires.
- Interactive MCP — human/agent-in-the-loop, non-deterministic, can’t gate a PR, and has its own env fragility (this session: the MCP needs Chrome at
/opt/google/chrome, which isn’t installed; the runner’s bundledchromium-1223is fine). Keep it for ad-hoc “open this screen / triage this failure,” not for the recurring loop. - If you want interactive MCP available for debugging, fix it once: install Chrome on the box or set the MCP launch to
--browser chromiumto reuse Playwright’s binary.
5. Phased worklist (maps blockers → tasks)
Phase 1 — Keystone (unblocks everything).vite.config.ts: honor injectedVITE_SUPABASE_URL/key whenisE2Eor a preview-branch URL is present; keep the project-ID map as the default. (small change)- Production-safety guard: extend
environment-isolation+check-no-prod-refsto prove a non-prod URL can never bake into a prod build, and that prod can never bake a preview URL. (the careful part) - Verify the built bundle’s baked URL in CI (assert it matches the intended target).
seed:e2e script reusing the integration seeders (Proliant Stage-2, WENO creds, plus the F4a-fixed users). Resolve the 04_hr–12 enum-drift exclusion or carve a dedicated E2E seed module.
Phase 3 — Environment.
5. Enable Supabase branching + Vercel preview wiring; set PROLIANT_FIXTURE_TRANSPORT=1 as branch function env; synthetic-only PHI guard + cost guardrail.
Phase 4 — Wire the loop.
6. Render-smoke autoheal on every PR (cheap) against the preview branch; full-flow seeded autoheal on e2e-full/merge-queue; keep the kill-switch.
7. Tie to the AC gate: a green autoheal flow promotes its @verifies AC (spec → UI-verified → verified), mirroring the integration path built this session.
Phase 5 — Inner loop (optional).
8. Document E2E_LOCAL=1 local-hermetic mode (G1 local branch) + heal-e2e <core> for fast local iteration.
6. Decisions (taken 2026-06-02)
- Branching cost/scope → per-PR ephemeral branches (hermetic; accept the $ + spin-up cost), with a per-PR concurrency guardrail.
- Vendor calls on previews → fixture-transport (
PROLIANT_FIXTURE_TRANSPORT=1, deterministic); live sandbox deferred until BAA/creds exist. - Pilot flows → Proliant payroll-run wizard + WENO prescribe (this session’s specimens).
- Autoheal autonomy → kill-switch default off (propose-only PRs); revisit after it proves stable.
- Gate strength → render-smoke advisory first; promote to blocking once stable.
Progress
- Phase 1 (G1 keystone) — DONE 2026-06-02.
scripts/build/supabase-public-env.ts(chooseSupabaseEnv) is the guarded override; wired intovite.config.ts. Two-direction prod-safety guard unit-tested intests/unit/build/supabase-public-env.test.ts(6/6) and verified at the build level (E2E build with an injectedVITE_SUPABASE_URLbakes the local URL; a prod-context build refuses the override). This also un-breaks thee2e-full.ymlE2E_VITE_SUPABASE_URLsecret (was a no-op). - Phase 2 (local-hermetic seed + first real browser pass) — DONE 2026-06-02.
npm run seed:e2eplants test-org-alpha’s Proliant flow (active integration +custom_fields.company_id+ calendar + run-state + company mapping +proliant_integration_enabledflag + admin default org). Verified via a live browser pass: dev server pointed at the LOCAL stack via the keystone (E2E_SUPABASE_OVERRIDE=1, confirmed — login hit 127.0.0.1:54321, zero*.supabase.co), admin auto-landed in test-org-alpha, and/hr/payroll/runs/proliantrendered the wizard configReady (Step 1 of 7) — not the “not configured” empty state. Integration page un-gated; WENO card clean; no REAL_BUG. First real browser test of the flow on the PR’s own code, reproducible viaseed:e2e+ the keystone.- Cosmetic/pre-existing nit surfaced:
<button>-in-<button>hydration warning in the shared platform WizardShell timeline (StepCircle/ TimelineLayout) — affects all wizards, not HR-44; low priority.
- Cosmetic/pre-existing nit surfaced:
- Branching confirmed (env-model note): per-PR Supabase preview branches are live (PR #705 has one) but frontend-pinned to dev2 by design (runbook VERCEL_SUPABASE_ENV_ALIGNMENT.md); they validate migrations, they are not the preview frontend’s backend. Hence local-hermetic is the chosen browser-test substrate (no design reversal).
- Phase 4a (committed local-hermetic lane) — DONE 2026-06-02.
npm run test:e2e:local(playwright.local.config.ts+proliant-local.spec.tsglobal-setup.local.ts) is a repeatable gate: webServer builds the app local-target via the keystone, globalSetup runsseed:e2e, and the spec logs in + asserts the Proliant wizard rendersconfigReadyand the integration page is un-gated. 2 passed against the local backend; skips cleanly when the stack is down. Render-smoke scope (no AC backlink — full start→submit drive is the next tier).
- Surfaced + fixed a real CSP gap: the preview bundle’s
connect-srcallowed only*.supabase.co, so a local-targeted preview was CSP-blocked from the local Supabase origin (login silently failed). vite.config now allows127.0.0.1:54321/localhost:54321under PLAYWRIGHT_E2E only (never prod).
- Phase 4b (autoheal wiring) — DONE pending CI validation 2026-06-02.
.github/workflows/e2e-local-hermetic.ymlrunsnpm run test:e2e:localon the DGX runner (labele2e-local/ manual dispatch), skip-safe when the stack is down, with the same propose-onlyencore-test-triagehealer + kill-switch ase2e-full.yml. The local config now emits a JSON report for the healer. Mirrors the proven e2e-full shape; needs one real DGX CI run to validate the local-stack-on-runner assumption (only run locally so far). - Full start→submit AC-8 UI proof — decided (b) network-mock; clean local
lane deferred. A real local-hermetic AC-8 browser drive can’t inject the
x-proliant-fixturesheader the integration tests use, so the choice was (a) Proliant sandbox creds (BAA) vs (b)page.routemocking the function — we chose (b) (deterministic, no BAA). The full mocked drive already exists intests/e2e/hr/proliant-payroll-run.spec.ts(login → mocked function/REST → asserts “payroll posted”), but wiring it into the local lane was reverted: it imports@chromatic-com/playwright(snapshot-after-each errors in this lane) and gates on a mocked “Run Payroll” heading that fights the real local backend. Follow-up: a chromatic-free local full-drive spec (reuse the mock fixtures,@playwright/testimport, real local login). Until then: the drive logic is proven by that mocked spec (default lane) and the real server-side AC-8 bypayroll-run.int.test.ts(which the verify-acs gate promotes); the local lane is the UI reachability (render-smoke) proof. No@verifies AC-8on a Playwright spec — verify-acs runs vitest, not Playwright. - Minor follow-ups: cosmetic
<button>-in-<button>hydration warning in the shared platform WizardShell timeline (all wizards); WENO cred seeding for a local WENO browser pass. - Phase 3 (frontend→branch) intentionally deferred (would reverse the dev2 pinning).
7. Risks / guardrails
- Prod-safety of the URL override (G1) is the highest risk — a bug could point a prod build at a test DB or vice-versa. Two-direction guard tests are mandatory before this ships.
- PHI on preview branches — synthetic seed only; enforce in the seed + a check.
- Cost/flake of branching — guardrail per-PR concurrency; reuse
reuseExistingServer-style short-circuits. - Healer scope creep — keep the ≤5-test cap + kill-switch; healer proposes PRs, never auto-merges (already the case).