Hallucination dogfood
Why
.izri/izri.ymlfor Izri itself looks the way it does, and what the deployer has to provision.
TL;DR
Izri is tested like any other project that declares runtime services in its .izri/izri.yml:
services:
postgres: { image: postgres:15, env: { POSTGRES_PASSWORD: postgres } }
redis: { image: redis:7 }
The runner provisions both containers via testcontainers (3.8M weekly downloads — ecosystem standard) before runtime.start fires. Well-known service names produce conventional env vars (DATABASE_URL for postgres, REDIS_URL for redis), so the SUT's start command needs no surgical wiring.
Single requirement: Docker socket access
The runner image needs docker available and the Docker daemon reachable — same as any CI environment that runs container-based services (GitHub Actions, GitLab CI, CircleCI all require this). Two mechanisms:
- Docker socket mount (
-v /var/run/docker.sock:/var/run/docker.sock) — easiest. The runner uses the host's Docker daemon. Containers it starts are siblings, not children. - Docker-in-Docker (
--privileged+ dind sidecar) — fully isolated but heavier.
Both are deployment concerns; testcontainers auto-discovers either. The reaper testcontainers ships with cleans up orphaned containers on runner crash, so SIGKILL won't leak state.
End-to-end flow
runner job
→ clone PR
→ services: spin up postgres + redis via testcontainers
→ exposes DATABASE_URL, REDIS_URL in env overlay
→ start: install + migrate + dev:packages/api/web
→ ready: http://localhost:5173 (2xx)
→ test: playwright --reporter=junit, PLAYWRIGHT_COVERAGE=1
(monocart-reporter wraps page.coverage; emits LCOV)
→ results: apps/web/test-results/junit.xml
→ coverage: apps/web/test-results/lcov.info
→ services: stop + remove containers (finally block; idempotent)
Auto-exposed env vars
The runner exposes connection info to the SUT via two layers:
Generic (every service):
IZRI_SERVICE_<NAME>_HOST— typicallylocalhostIZRI_SERVICE_<NAME>_PORT_<INTERNAL>— mapped host port for each exposed container port
Conventional (well-known names):
| Service name | Auto-exposed env var | Format |
|---|---|---|
postgres / postgresql |
DATABASE_URL |
postgresql://postgres:<password>@<host>:<port>/<db> |
mysql |
DATABASE_URL |
mysql://root:<password>@<host>:<port>/<db> |
redis |
REDIS_URL |
redis://<host>:<port> |
mongo / mongodb |
MONGO_URL |
mongodb://<host>:<port> |
Password / DB defaults come from the service's env: block when set, otherwise sensible defaults (postgres/postgres for Postgres, etc.). The SUT reads DATABASE_URL directly; no service-name-aware code in the user's app required.
Adding a new service Izri (or any project) needs
Just declare it:
services:
elasticsearch:
image: elasticsearch:8.11.0
env: { discovery.type: single-node, xpack.security.enabled: 'false' }
ports: [9200]
waitFor: { log: 'started' }
For unknown names there's no *_URL convenience — your start command references it via ${IZRI_SERVICE_ELASTICSEARCH_HOST}:${IZRI_SERVICE_ELASTICSEARCH_PORT_9200}.
Known limitations
- Backend coverage for Izri itself — Slice 0 captures frontend (Playwright) only.
NODE_V8_COVERAGEwould require the API process to be a Node subprocess of the test runner; Playwright'swebServerwould have to be the spawn point. Frontend-only for v1. - Source-map resolution edge cases — monocart's
sourcePathtransform strips the bundled-host prefix back toapps/web/.... If a React Router build emits chunks with a path the transform doesn't expect, those entries land innotInstrumented. TheentryFilterglob mitigates by ignoring anything outsideapps/web/app/**. - Concurrent runner replicas — services are per-job (testcontainers gives each container a unique name) so replicas don't collide; but image pulls add startup latency. Warm caches on the host help.