14 KiB
E2E Readiness — Handoff (2026-06-01)
Single-source pickup doc for the boiler e2e fix effort. Target: boiler.saas-ctrl.com
(staging on Hetzner staging-1, 46.225.5.93). Companion memory files:
boiler-deploy-seed.md, boiler-e2e-remaining-tail.md (in the agent memory dir).
Current status
Full local run against boiler (set -a; source .env.e2e-readiness.boiler; set +a; npx playwright test):
| Run | pass / fail / skip |
|---|---|
| baseline (start of effort) | 108 / 54 / 46 |
| full-run-8 | 157 / 10 / 41 |
| full-run-9 (latest) | 163 / 15 / 30 |
Headline: +55 pass vs baseline. Fail rose 10→15 only because the workflow-execution fix (below) un-skipped ~11 previously-gated workflow tests — they now run and some fail on detail contracts. Not regressions.
All code fixes are on main (latest pushed commit 379a5b25). The hard infrastructure
is solved; what remains are detail contracts + one real UI bug.
2026-06-01 FINAL RESULT — merged to main and deployed to boiler
Full single-worker run with --retries=2 (CI-equivalent) = 178 pass / 0 fail / 2 flaky
(pass on retry) / 28 skip. The 2 flaky are serial-only and pass in isolation + on retry:
read-only plugin permissions (app.plugins.use) and a registration login-rate-limit test
(10/60s). All real product bugs and test-contract issues below are fixed. Commits on main:
71686c25 (getWorkflowExecution 500→404), ef7f4ee4 (run-dropdown Menu.Item Fragment 500),
626abd9d (prune stale role→permission grants), 5261bf98 (z-50/CSRF/canvas), 87b997fd
f93ba964+8de012ef(e2e test contracts). Re-seed after every deploy (/tmpephemeral).
2026-06-01 follow-up (investigation detail)
Re-investigated the plugin-settings + workflow-run mobile-a11y specs against boiler with fresh re-seeds. The earlier read-only diagnosis was partly wrong; running with traces corrected it. Net: 8 of the 13 remaining were genuine test-contract / single-fetch bugs (now fixed + locally verified passing), 3 are product bugs (fixed in code, need a web deploy to validate), and 3 are staging environment / RBAC / seed issues (not code).
Fixed in tests — verified GREEN locally (no deploy needed):
- All RR7 single-fetch interceptors now match
<path>.dataand return a turbo-stream body, notapplication/json(faked JSON breaks the decoder souseActionData()is empty and the UI never renders). New helpersingleFetchActionData(payload, status?)intests/e2e/utils/navigation.tscalls the realturbo-streamencode({data})(byte- identical to the provensingleFetchActionError). Fixed: plugin consent/lifecycle/revoke/ stale-review (capturePluginPost+ inline 409/400 interceptors; also matches thefetch(".")parent-dir for the iframe write), and workflow manual/api/stream waiting-block continuations + manual-error. - Stale-review (
:173): the readiness plugin has 2+ missing capabilities (audit + a hook subscription); approval needs every checkbox confirmed. Test now checks all (mirrors the consent test). Approve-button-disabled was the real blocker, not the interceptor. - Invalid-JSON (
:780): Monaco auto-closed a typed{into a valid{}AND a bare wrapper click left keystrokes unrouted. Fixed by focusing the.monaco-editoredit context, theninsertText("not valid json")(non-bracket, paste-like), asserting the model value.
Product bugs — fixed in code, NEED a web deploy + re-seed to validate:
- Run dropdown obstructed by ReactFlow (
:303, was:315): toolbar lacked a stacking context. Addedrelative z-50to the builder toolbar inworkflows.$id.index.view.tsx. - Stream waiting-block continuation POST missing CSRF (
:567):WorkflowStreamProgress.tsxonSubmitWaitingBlocknever calledappendCSRFToken(form)(manual/api views do). Added it. - Execution-detail builder canvas hidden on mobile (
:617): ReactFlow needs a definite height;workflows.$id.executions.view.tsxused indefinitemin-h/flex-1. Applied the provenh-[70vh] lg:h-auto lg:min-h-0 lg:flex-1(same as the index view).
Staging environment / RBAC / seed — NOT code bugs (CI lane is the arbiter):
:175builder "stays usable" → e2e tenant user is redirected to/unauthorized/app.settings.roles.set/<tenant>(HTTP 200, soskipIfUnavailabledoesn't skip). The seeded user is missing role permissions on staging (the "roles flag seed" gap).:461settings iframe → the readiness plugin is Disabled (state drifted across the serial run; the settings detail route is correctly unreachable while disabled). Needs per-test enable/restore or abeforeEachre-seed of the plugin's enabled state.:669read-only user sees mutation controls → theUserrole carries a staleapp.plugins.managegrant.syncDefaultRolesAndPermissionsis add-only (never prunes role→permission links), so the 2026-05-26 plugin-permission split never propagated to tenants seeded before it. Immediate: prune the stale grant on staging. Durable: make the sync prune links no longer indefaultPermissionSeedDescriptors[].inRoles.
What was fixed this effort
Real product bugs
- Outbox BullMQ jobId contained
:(2b6ff2bf).jobId: \outbox:${id}`— BullMQ rejects:("Custom Id cannot contain :"), so EVERY async outbox dispatch failed and dead-lettered (workflow.execution.requested, workflow.trigger.event, plugin.hook.*, audit.event.create). Unit tests mockedqueue.addso it never surfaced. Fixed to-inapps/worker/src/outbox/bootstrap.tsANDworkflow-execution-stalled-reconciler.tsbuildOutboxJobId(must agree).outbox-receiver.tsidempotency key keeps:` (not a BullMQ id). This was breaking ALL async delivery in production, not just tests. - Language switch was a no-op (earlier, root.tsx) — setLocale didn't write the
_i18ncookie remixI18Next.getLocale reads. - Misc a11y/contrast cluster (rose-500→600, focus-return, etc.) — see git log.
Coolify config changes (staging only — applied via API, NOT in repo)
The workflow-execution + plugin tests were blocked by deployment config, not code:
- worker-general
WORKFLOW_TRIGGER_EXECUTOR_MODE=native(washttp). In http mode the worker POSTs to the web's/internal/api/workflows/executions/<id>/start, but that route is deliberately gated out ofroutes.ts(internal/api/__workflows/**, enforced by a test — do NOT un-gate it). native mode runs safe-core workflows (manual/log/if/switch/ httpRequest/email/rowGet…) directly in the worker via prisma. This made workflow execution work (the outbox-drain canary goes green). PLUGIN_BINARY_BASE_PATH=/tmp/e2e-pluginson boilerplate-web + worker-general. The image default/app/var/pluginsdoesn't exist and is unwritable for uid 1000node; the seed writes the plugin bundle to/tmp/e2e-plugins. Set this so the web reads it. Made the plugin settings iframe render.- boilerplate-web
custom_network_aliases=boilerplate-webso the cluster-internal URLhttp://boilerplate-web:8080resolves (the public fqdn returns curl000from inside — hairpin NAT). (Only needed if you ever switch the worker back to http mode.)
Test fixes (on main)
tests/e2e/utils/navigation.tssingleFetchActionError()— builds the real RR7 single-fetch turbo-stream error envelope (text/x-script+ body[{"_1":2},"data",{"_3":4},"error","msg"]\n- header
X-Remix-Response: yes— without that header the client renders a 500 boundary).
- header
tests/e2e/utils/axe.ts—settleAnimations()waits on Web Animationsfinishedbefore scanning (kills transient color-contrast flakes); filtersscrollable-region-focusableonrole="menu"(Radix false positive).- input-editor 320px: focus Monaco edit context + assert the editor MODEL value (DOM is
virtualised). executions-mobile: assert the
.filter({visible:true})Executions label. readinessPluginId passed into the iframeevaluate. signout race, portals skip, drawer reopen.
Coolify / infra access (found in the infra repo, ~/Documents/GitHub/infra)
- API:
https://saas-ctrl.com/api/v1, Bearer =~/.cli-tokens/coolify-token(works). - App UUIDs: web
mrf8lj70kmy9hqprx9dd6w7q, apie109dzc3xzjxi54llfje4tob, worker-generalwzshv933n2nhy0cvsxu8bqmm(drains outbox + runs workflow executor), worker-plugino10eqbjd7hd49d2iuyjataqo, -import/-mail/-webhook. - Env API gotcha:
POST /applications/{uuid}/envs {key,value}and PATCH-by-key BOTH create → dupes. Always GET uuids andDELETE /applications/{uuid}/envs/{env_uuid}extras. - Redeploy:
GET /applications/{uuid}/deploy(add?force=trueto re-pull image). - SSH host:
boilerplate-staging(root@46.225.5.93, key~/.ssh/node1). - DB query: throwaway
postgres:16-alpineon thecoolifydocker network; strip?pgbouncer=truefrom$DATABASE_URL; columns are quoted camelCase.
Re-seed (REQUIRED after every web deploy — /tmp is ephemeral)
WEB=$(ssh boilerplate-staging "docker ps --format '{{.Names}}\t{{.Image}}' | grep /web: | cut -f1 | head -1")
ssh boilerplate-staging "docker exec -w /tmp -e E2E_SEED_ALLOW_PRODUCTION=1 -e E2E_LOCAL_READINESS_SEED=1 \
-e PLUGIN_BINARY_BASE_PATH=/tmp/e2e-plugins $WEB node /myapp/packages/db/dist/e2e-readiness-seed.server.js"
Remaining 15 failures — categorized with pointers
A. Workflow run-detail (7) — newly running after the execution fix
File tests/e2e/workflow-builder-run.mobile-a11y.spec.ts. The execution CORE works (drain
canary + execution-status + status-toggle-contract pass with E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1).
These fail on continuation/error contracts — likely 1-2 shared root causes in the run flow:
:462manual waiting-block continuation posts raw input:539API waiting-block continuation posts JSON input:579stream run waiting-block continuation:629manual run result deep-links to execution details (gated by E2E opt-in):751manual run remains on-page when execution action returns an error:796manual run rejects invalid JSON through the real action:900keeps builder and manual run reachable at 320px START HERE: run one (e.g.:796) withE2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1, read the failure; the waiting-block ones sharecontinueExecution/wait-for-input handling. Native executor supports a subset of blocks (seedocs/worker.md) — confirm wf_waiting_123's blocks are in it, else native fails-loud on unsupported graph semantics (would explain waiting-block fails).
B. Plugin-settings POST/capability contracts (5)
File tests/e2e/plugin-settings.mobile-a11y.spec.ts. The iframe now RENDERS (binary path fixed).
:49capability consent review — "capability grant POST captured: null" (the grant POST never fires → needs the capability in a pending-review state in the seed, or the approve control isn't rendering).:173stale capability review error:332lifecycle controls non-mutating interception contract:461settings iframe sandbox/CSP — readinessPluginId test bug fixed; now fails on the settings-write POST not being captured (line ~546) — likely needsE2E_ALLOW_PLUGIN_SETTINGS_WRITEopt-in OR the parent doesn't forward the iframe message (channelId/pluginId validation).:617capability revoke controls non-mutating form contract START HERE: these likely share a capability-grant/seed-state root. Check what capability state the seed creates vs what the consent-review UI needs (a pending grant request).
C. Real UI bug (1) — run dropdown obstructed by ReactFlow
tests/e2e/workflow-builder-run.mobile-a11y.spec.ts:315 "run dropdown stays keyboard reachable".
Diagnosed: the ui.workflows.run.dropdown.button (HeadlessUI Menu.Button in
apps/web/app/modules/workflowEngine/components/workflows/buttons/WorkflowRunDropdown.tsx) is
visible but click() TIMES OUT and aria-expanded stays false even on a DOM click — the
ReactFlow canvas overlays it (captures pointer + keyboard) on the builder page at mobile width.
This is a genuine product bug (z-index / pointer-events / layout on the builder), needs a fix +
web deploy. NOT a test bug.
D. Test-isolation (1) — status toggle
tests/e2e/workflow-builder-actions.spec.ts:155 "persists workflow status toggles". The toggle
now PERSISTS (native mode made the status mutation work), so the persisted state leaks between
runs and the test's read-initial/restore logic races (error flips between "expected true got
false" and vice-versa across runs). Fix: make the test reset wf_123 status deterministically, or
the seed should pin a known status each run.
E. Cross-suite flake (1)
"guards seeded tenant account membership before dashboard" — passes in isolation AND its own full file; only fails in the full serial run (cumulative login rate-limit, 10/60s). CI lane's retries absorb it. Not a real fail.
How to run / verify
- Full local:
cd boilerplate; set -a; source .env.e2e-readiness.boiler; set +a; npx playwright test --reporter=json > /tmp/run.json - Workflow execution tests need
export E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1. - The authoritative "everything on" run is the CI readiness lane (
.gitea/workflows/e2e-readiness.yml,workflow_dispatchwith the operator opt-ins: workflow_outbox_drain, plugin_settings_write, plugin_lifecycle_mutation) which setsREQUIRE_E2E_READINESS=1(fail-closed).
Do-not-repeat notes
- Do NOT un-gate
internal/api/__workflows/**in routes.ts (a test enforces the exclusion; the intended path is native executor mode). - Do NOT use a global
<style>to freeze CSS transitions in the axe helper — it perturbs later Radix close-animation focus timing. Use the Web Animationsfinishedwait (already in place). - Coolify env POST/PATCH both CREATE — always dedupe by deleting extra env uuids.