Docs /development/hallucination-dogfood

Hallucination dogfood

Why .izri/izri.yml for 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 — typically localhost
  • IZRI_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_COVERAGE would require the API process to be a Node subprocess of the test runner; Playwright's webServer would have to be the spawn point. Frontend-only for v1.
  • Source-map resolution edge cases — monocart's sourcePath transform strips the bundled-host prefix back to apps/web/.... If a React Router build emits chunks with a path the transform doesn't expect, those entries land in notInstrumented. The entryFilter glob mitigates by ignoring anything outside apps/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.

Reading this with an agent? /docs/development/hallucination-dogfood.md serves the raw markdown.

All docs