11 KiB
worktree-test-stacks Specification
Purpose
Define the contract by which the test/ Docker Compose stack supports running isolated, concurrent instances across multiple git worktrees of the same repository. Each worktree gets its own collision-free host ports, project name, secrets, and templated URLs so developers (and LLM agents) can run parallel stacks without stomping on each other.
Requirements
Requirement: Compose stack accepts per-worktree port and hostname overrides
The test/compose.yaml file SHALL parameterize every host port binding and the Keycloak hostname using ${VAR:-default} substitution so that a test/.env file controls the running stack while preserving the current default ports when no .env is present.
Scenario: Default ports preserved when no .env exists
- WHEN a developer runs
docker compose -f test/compose.yaml up -dfrom a worktree without atest/.envfile - THEN Postgres binds to host port 5432, Valkey to 6379, Keycloak to 8080, Temporal to 7233, Temporal UI to 8233, and FedWiki to 80
- AND the Keycloak hostname is
keycloak.localhost
Scenario: .env values override defaults
- WHEN
test/.envdefinesPOSTGRES_PORT=5532,KEYCLOAK_PORT=8180,TEMPORAL_PORT=7333,TEMPORAL_UI_PORT=8333,FEDWIKI_PORT=8181,VALKEY_PORT=6479 - AND the developer runs
docker compose -f test/compose.yaml up -d - THEN each service binds to the host port specified in
.envinstead of the defaults
Scenario: COMPOSE_PROJECT_NAME isolates network and volumes
- WHEN
test/.envsetsCOMPOSE_PROJECT_NAME=mc-llm-aand the stack is brought up - THEN Docker networks, volumes, and container names are prefixed with
mc-llm-a - AND containers from a stack with a different
COMPOSE_PROJECT_NAMEdo not appear on the same network
Requirement: Keycloak seed reads URLs from the environment
The test/seed/keycloak/seed-keycloak.sh script SHALL accept the member-console base URL, temporal-ui base URL, and any other host-port-dependent URLs from environment variables exported by the compose service definition, so that seeded redirect URIs and web origins match the running stack's host ports.
Scenario: Seed uses default URLs when env vars unset
- WHEN the seed script runs without
MC_BASE_URLorTEMPORAL_UI_URLset - THEN the member-console client is created with
redirectUris: ["http://localhost:8081/*"]andwebOrigins: ["http://localhost:8081"] - AND the temporal-ui client is created with
redirectUris: ["http://localhost:8233/*"]andwebOrigins: ["http://localhost:8233"]
Scenario: Seed uses templated URLs when env vars are set
- WHEN the seed script runs with
MC_BASE_URL=http://localhost:8090andTEMPORAL_UI_URL=http://localhost:8333 - THEN the member-console client is created with
redirectUris: ["http://localhost:8090/*"]andwebOrigins: ["http://localhost:8090"] - AND the temporal-ui client is created with
redirectUris: ["http://localhost:8333/*"]andwebOrigins: ["http://localhost:8333"]
Scenario: Seed remains idempotent with templated URLs
- WHEN the seed script runs a second time against an already-seeded Keycloak with the same env vars
- THEN the script exits with status 0
- AND existing clients have their
redirectUrisandwebOriginsupdated to match the env-templated values rather than skipped
Requirement: Bootstrap script generates a per-worktree .env
The test/bootstrap-stack.sh script SHALL generate a test/.env file containing port assignments, COMPOSE_PROJECT_NAME, Keycloak hostname, member-console base URL, and temporal-ui base URL based on a slug derived from the current worktree.
Scenario: Bootstrap derives slug from worktree directory basename
- WHEN the bootstrap script is invoked without arguments from a worktree at
/home/user/Repos/mc-llm-a - THEN the script uses
mc-llm-aas the slug - AND writes
COMPOSE_PROJECT_NAME=mc-llm-atotest/.env
Scenario: Bootstrap accepts an explicit slug argument
- WHEN the bootstrap script is invoked as
./test/bootstrap-stack.sh custom-slug - THEN the script uses
custom-slugas the slug regardless of the worktree directory name - AND writes
COMPOSE_PROJECT_NAME=custom-slugtotest/.env
Scenario: Bootstrap allocates ports deterministically by slug hash
- WHEN the bootstrap script computes the port allocation for a given slug
- THEN the slot is
fnv32(slug) mod 200 - AND each service port equals
default_port + slot * 50 - AND running bootstrap a second time with the same slug in a clean environment produces the same port assignments
Scenario: Bootstrap recovers from port collisions
- WHEN the bootstrap script's hashed slot would assign ports that are already bound on the host
- THEN the script increments the slot (mod 200) until a slot whose ports are all free is found
- AND the chosen slot is persisted in
test/.envso subsequent runs in the same worktree do not re-probe
Scenario: Bootstrap is idempotent when .env already exists
- WHEN the bootstrap script is invoked in a worktree where
test/.envalready exists - THEN the script does not modify
test/.env - AND prints the existing port and URL summary
- AND exits with status 0
Scenario: Bootstrap prints a port and URL summary
- WHEN the bootstrap script completes successfully
- THEN the script prints a table to stdout listing the stack name, every assigned host port, and the member-console, Keycloak, Temporal UI, and FedWiki URLs that the developer should use
Requirement: Bootstrap propagates secrets from the main worktree
The test/bootstrap-stack.sh script SHALL copy test/secrets/ from the main git worktree into the current worktree when test/secrets/ does not exist locally, so that newly created worktrees inherit the gitignored test credentials.
Scenario: Secrets copied from main worktree on first bootstrap
- WHEN the bootstrap script runs in a worktree that has no
test/secrets/directory - AND the main worktree contains
test/secrets/stripe-api-keyandtest/secrets/stripe-webhook-secret - THEN the script copies both files into the current worktree's
test/secrets/
Scenario: Secrets not overwritten on re-run
- WHEN the bootstrap script runs in a worktree that already has
test/secrets/ - THEN the script does not modify or overwrite the existing
test/secrets/contents
Scenario: Bootstrap fails clearly when main has no secrets
- WHEN the bootstrap script runs in a worktree that has no
test/secrets/ - AND the main worktree also has no
test/secrets/directory or it is empty - THEN the script exits with a non-zero status
- AND prints an error message identifying the expected source path and instructing the developer how to obtain the credentials
Requirement: Teardown script returns the worktree to a clean state
The test/teardown-stack.sh script SHALL bring down the compose stack with volumes removed, delete test/.env, delete test/secrets/, and delete test/testdata/, leaving the worktree as if bootstrap had never run.
Scenario: Teardown removes containers, volumes, and generated files
- WHEN the teardown script is invoked in a worktree where bootstrap has run and the stack is up
- THEN the script runs
docker compose down -v --remove-orphansagainst the stack - AND deletes
test/.env,test/secrets/, andtest/testdata/
Scenario: Teardown is safe to run when nothing is up
- WHEN the teardown script is invoked in a worktree where bootstrap has not been run
- THEN the script exits with status 0 without error
- AND does not attempt to delete files that do not exist
Requirement: Application reads MC_-prefixed env vars to override mc-config.yaml
The bootstrap script SHALL produce a test/.env whose MC_* variables, when exported into the shell that runs member-console start, override the corresponding mc-config.yaml keys via Viper's existing AutomaticEnv binding.
Scenario: Environment overrides config file port
- WHEN
test/.envdefinesMC_PORT=8090and the developer exports it before runningmember-console start --config test/mc-config.yaml - THEN the application listens on port 8090 instead of the
port: 8081value in the config file
Scenario: Environment overrides database DSN
- WHEN
test/.envdefinesMC_DB_DSN=postgres://member_console:member_console@localhost:5532/member_console?sslmode=disable - AND the developer exports it before running
member-console start --config test/mc-config.yaml - THEN the application connects to Postgres on host port 5532 instead of the 5432 in the config file
Requirement: Multiple worktrees run isolated stacks concurrently
When two or more worktrees of the same repository each run ./test/bootstrap-stack.sh followed by docker compose -f test/compose.yaml up -d, all stacks SHALL run concurrently without port collisions, without sharing Postgres or Temporal state, and without sharing Keycloak realms.
Scenario: Two worktrees up at the same time
- WHEN worktree
mc-llm-aand worktreemc-llm-bboth bootstrap and bring up their stacks - THEN every host port assigned to
mc-llm-adiffers from every host port assigned tomc-llm-b - AND
docker psshows two distinct sets of containers prefixedmc-llm-a-*andmc-llm-b-* - AND the Postgres database in
mc-llm-adoes not contain rows written by the application running againstmc-llm-b
Scenario: Login flow works against a non-default port set
- WHEN a stack has been bootstrapped with a non-zero port offset (Keycloak on a port other than 8080, member-console on a port other than 8081)
- AND the developer completes an OIDC login by visiting the member-console URL printed by bootstrap
- THEN the login succeeds end-to-end including redirect back to the templated
MC_BASE_URL
Requirement: Agent-targeted documentation directs agents to bootstrap before compose
The repository SHALL provide a test/AGENTS.md file that explains the parallel-stack contract to LLM agents, and SHALL reference this contract from test/README.md and the top-level CLAUDE.md.
Scenario: AGENTS.md exists in test/ with required content
- WHEN an agent reads
test/AGENTS.md - THEN the file states that
./test/bootstrap-stack.shmust be run beforedocker compose up -d - AND describes that the script is idempotent
- AND lists which URLs and ports the agent should use after bootstrap completes
- AND documents that Stripe webhook scenarios must not be run in parallel across stacks
Scenario: README and CLAUDE.md point at AGENTS.md
- WHEN an agent reads
test/README.mdor the top-levelCLAUDE.md - THEN each file contains a reference instructing the reader to consult
test/AGENTS.mdand run./test/bootstrap-stack.shbefore bringing the stack up
Requirement: test/.env is gitignored
The repository's .gitignore SHALL exclude test/.env so that per-worktree port assignments are not accidentally committed.
Scenario: test/.env is not tracked
- WHEN a developer runs
git statusafter./test/bootstrap-stack.shhas createdtest/.env - THEN
test/.envdoes not appear as an untracked file - AND
git check-ignore test/.envreports the file as ignored