Live practice-review E2E
This setup runs the complete local path:
- Workspace creation and SCM sync.
- Job execution in Docker.
- LLM calls through the application proxy.
- Result persistence.
- Feedback delivery to a PR or MR.
The opt-in e2e profile enables these local-only capabilities:
| Affordance | Property |
|---|---|
| Passwordless dev sign-in | hephaestus.auth.dev-login-enabled=true |
Plain-http cookies (drops Secure + __Host-) | hephaestus.auth.cookie-secure=false |
| Self-service GitLab/GitHub workspace creation | hephaestus.workspace.creation-policy=SELF_SERVICE + hephaestus.features.flags.gitlab-workspace-creation=true |
| Persistable Connection credentials (PATs) | hephaestus.security.encryption-key (a fixed local dev key) |
| Dev review trigger | hephaestus.dev.trigger-enabled=true |
| PostgreSQL job executor | hephaestus.agent.enabled=true |
vp run dev:server:e2e starts Postgres and NATS before activating the profile.
Startup permits cookie-secure=false only with e2e active and prod absent.
The auth issuer must use HTTP, and the cookie name must not have the __Host- prefix.
All other profiles require Secure cookies.
Do not expose this profile outside a trusted development machine: it enables passwordless app-admin
login. The setup script accepts only loopback application and database URLs. The application server
still needs to be reachable from Docker through host.docker.internal, so enforce the boundary with
the host firewall or an isolated development network.
Local SCM simulation
For an isolated simulator, set hephaestus.e2e.scm-origin (environment variable
HEPHAESTUS_E2E_SCM_ORIGIN) to its exact http://127.0.0.1:<port> origin, without a trailing
slash. Startup rejects this setting unless e2e is active and prod is absent. Workspace
validation and GitLab preflight allow only that origin. Other destinations retain the normal
HTTPS and private-address restrictions.
GitLab workspace creation accepts only
GITLAB_DEFAULT_SERVER_URL, so set it to the same origin, and the creating account needs an
identity link on that origin. Preflight clients do not follow redirects and cannot send requests to a
different origin.
A Git fetch to that origin runs through JGit in the application's own process. Thus, it reaches the simulator exactly as the provider API calls do. The simulator must serve Git smart HTTP on the same loopback origin. This does not enable a GitHub simulator or supply missing provider APIs or historical replay.
Setup
# Terminal 1
vp run dev:server:e2e
# Terminal 2
vp run dev:webapp
# Terminal 3: read secrets without placing them in shell history.
read -rsp "SCM PAT: " E2E_GITLAB_PAT && echo && export E2E_GITLAB_PAT
read -rsp "LLM key: " E2E_LLM_KEY && echo && export E2E_LLM_KEY
export E2E_LLM_PRICING_MODE=PRICED
export E2E_LLM_INPUT_USD="$YOUR_CONTRACT_INPUT_RATE_PER_1M"
export E2E_LLM_OUTPUT_USD="$YOUR_CONTRACT_OUTPUT_RATE_PER_1M"
vp run dev:e2e:setup \
--account-login group/subgroup \
--repo group/subgroup/project \
--llm-base-url https://llm.example/v1 \
--model example-model
The script is idempotent. It creates or validates the workspace and connection, configures the model,
disables the review cooldown, and leaves three focused practices active. It requires an explicit cost
declaration: use PRICED with contract rates, or NO_CHARGE with E2E_LLM_PRICE_NOTE. Set
E2E_PR_ID to select a specific internal artifact ID. Otherwise, the script selects the newest MR or PR in the monitored repository. For GitLab, use the narrowest suitable group or subgroup because initial sync
covers every project below that path.
Other useful options are --provider, --server-url, and --app-url. The model connection uses
the Responses API. Use E2E_LLM_PROTOCOL=openai-completions for an endpoint that serves Chat
Completions alone, or E2E_LLM_AUTH_MODE=API_KEY when required by the provider.
The script reuses existing resources only when their immutable SCM and model-routing fields match.
Running the review
The setup output identifies the workspace and, when one has synced, the selected artifact. Use those IDs with the dev trigger:
JWT="$(curl -fsS -i -X POST http://localhost:8080/auth/dev-login \
-H 'content-type: application/json' \
-d '{"username":"e2e","admin":true}' \
| sed -nE 's/.*(__Host-)?HEPHAESTUS_AT=([^;]+).*/\2/p' | tr -d '\r')"
test -n "$JWT"
curl -fsS -X POST -H "Authorization: Bearer $JWT" \
"http://localhost:8080/api/dev/trigger-review?prId=<id>&workspaceId=<id>"
To test webhook ingestion too, configure hephaestus.webhook.external-url and a secret of at least 32
characters, then expose the webhook receiver through a trusted tunnel. GitLab group-hook registration
requires the appropriate group role and license.
The agent runs in a Docker sandbox (ghcr.io/hephaestus-build/agent-pi) and calls the LLM through
the in-app proxy, so provider keys never enter the sandbox.
A practice review always runs on an internal Docker network.
Only the worker that started it joins this network.
Thus, practice reviews need the worker role in a Docker container under its default hostname, as application-worker does.
A host-run server refuses them.
The conversation host is different.
When the worker runs outside any container, the host uses an ordinary bridge network and reaches the worker through host.docker.internal.
Runtime roles gives the rule.
No binding setting controls this.
Hephaestus posts feedback back to the MR.
The workspace's Practices → Practice reviews view shows the results behind it.
Live runner JUnit tests call the upstream provider directly. They do not cover application proxying, budget admission, durable usage accounting, sandbox execution, or SCM delivery.
Browser tests (Playwright)
webapp/e2e/ holds a @playwright/test harness (vp run --filter webapp test:e2e) that drives the
SPA over plain http through the dev-login — see the README.md in that directory. It uses the same
cookie-secure=false + XSRF-TOKEN wiring this page relies on.
Caveats
Plan & scope issuesis evaluated in PR context. Use a PR or MR.- Delivery needs a PR row with
author_id— synced MRs have it. A hand-seeded PR may not. - The sandbox runtime needs
/var/run/docker.sock.