# test/ — instructions for LLM agents If you are an automated agent working in this repository, read this before starting Docker. The test stack uses fixed default host ports; multiple worktrees running it concurrently will collide unless each generates its own isolated configuration. All commands in this doc assume your working directory is `test/` — that's where compose, the env file, and the binary's expected secret paths all resolve from. ## Required first step in any worktree ``` cd test ./bootstrap-stack.sh ``` The script: - Derives a slug from this worktree's directory name (override with `./test/bootstrap-stack.sh my-slug`). - Allocates a deterministic port slot via `FNV-32(slug) % 200`. Probes for collisions and advances if any candidate port is already bound on the host. - Copies `test/secrets/` from the main worktree if missing locally (this directory is gitignored, so fresh worktrees do not inherit it). - Writes `test/.env` with `COMPOSE_PROJECT_NAME`, host port assignments, and `MC_*` env vars that override `test/mc-config.yaml` via Viper. - Prints the URL/port table the developer should use. - **Idempotent**: re-running with `test/.env` already in place is a no-op that reprints the summary. ## After bootstrap ``` docker compose up -d set -a; . .env; set +a go run .. start --config mc-config.yaml ``` Both `keycloak-seed` (creates realm users) and `fedwiki-render` (resolves real KC UUIDs and templates `seed/fedwiki/*.tpl` into `testdata/fedwiki/`) are compose services that run automatically as one-shots on `up`. `fedwiki` waits for both to complete before starting. Keycloak silently regenerates the `id` field on POST /users, so FedWiki identity cannot be pinned at commit time — it's rendered after each realm seed. The member-console binary must run with `test/` as its working directory because `mc-config.yaml` references `secrets/stripe-*` via relative paths. Compose auto-loads `.env` from the current directory, so no `-f`/`--env-file` flags are needed. Use the URLs printed by bootstrap, not the defaults from `test/mc-config.yaml` — the YAML still says `port: 8081`, but the running process listens on whatever `MC_PORT` is in `test/.env`. ## Walking the operator UI with data The default workflow produces a clean, empty environment — good for fresh-user flows. To populate the operator panel (products, plan ladder, entitlement set, one active grant) for UX walkthroughs or operator-panel exploration, run the opt-in demo seeder from the repo root after `bootstrap-stack.sh`: ``` ./test/seed-demo.sh ``` Idempotent — re-running is a no-op. Person rows for bob/carlos/diana are not seeded (Keycloak doesn't honor pinned `sub` IDs); log in once as each demo user to populate them. See [`seed/member-console-demo/README.md`](seed/member-console-demo/README.md) for the catalog and rationale. ## Tearing down ``` ./teardown-stack.sh ``` Brings the compose stack down with volumes, removes `test/.env`, `test/secrets/`, and `test/testdata/`. Returns the worktree to a clean state for the next experiment. ## Keycloak user seeding `seed/keycloak/seed-keycloak.sh` creates users via `POST /admin/realms/{realm}/partialImport`, not `POST /admin/realms/{realm}/users`. Keycloak 26.x silently drops the `id` field on the latter (server-side ID assignment for least-privilege admin operations); `partialImport` preserves pinned IDs and is the supported escape hatch. See OpenSpec change `2026-05-11-keycloak-id-pinning-fix` for the empirical confirmation and design rationale. ## Temporal namespace The compose stack sets `SKIP_DEFAULT_NAMESPACE_CREATION=true` on the `temporal` service, so a one-shot `temporal-seed` service registers the `default` namespace via the internal frontend on first `up`. If member-console fails with `Namespace default is not found`, the seed service didn't run yet — `docker compose ... up -d temporal-seed` and re-check `docker compose ... logs temporal-seed`. ## Constraints - **Do not run Stripe webhook scenarios in parallel across stacks.** All stacks share the same Stripe test account via `test/secrets/stripe-*`. Webhook delivery to one stack is global to that account; running two stacks through webhook flows simultaneously gives undefined results. - **Do not edit `test/.env` by hand.** Re-run bootstrap (after deleting the file) if you need to change ports. - **Do not remove the `${VAR:-default}` fallbacks from `compose.yaml`.** They preserve the single-stack default workflow when no `.env` is present. ## What `bootstrap-stack.sh` writes to `test/.env` ``` COMPOSE_PROJECT_NAME= SLOT=<0..199> POSTGRES_PORT, VALKEY_PORT, KEYCLOAK_PORT, TEMPORAL_PORT, TEMPORAL_UI_PORT, FEDWIKI_PORT KC_HOSTNAME, MC_BASE_URL, TEMPORAL_UI_URL MC_PORT, MC_DB_DSN, MC_VALKEY_ADDR, MC_OIDC_IDP_ISSUER_URL, MC_TEMPORAL_HOST, MC_TEMPORAL_OAUTH_TOKEN_URL ``` Compose reads the first three blocks. Viper reads the `MC_*` block via the existing `SetEnvPrefix("MC")` + `AutomaticEnv()` configuration in `cmd/root.go`.