Files

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 -d from a worktree without a test/.env file
  • 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/.env defines POSTGRES_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 .env instead of the defaults

Scenario: COMPOSE_PROJECT_NAME isolates network and volumes

  • WHEN test/.env sets COMPOSE_PROJECT_NAME=mc-llm-a and 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_NAME do 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_URL or TEMPORAL_UI_URL set
  • THEN the member-console client is created with redirectUris: ["http://localhost:8081/*"] and webOrigins: ["http://localhost:8081"]
  • AND the temporal-ui client is created with redirectUris: ["http://localhost:8233/*"] and webOrigins: ["http://localhost:8233"]

Scenario: Seed uses templated URLs when env vars are set

  • WHEN the seed script runs with MC_BASE_URL=http://localhost:8090 and TEMPORAL_UI_URL=http://localhost:8333
  • THEN the member-console client is created with redirectUris: ["http://localhost:8090/*"] and webOrigins: ["http://localhost:8090"]
  • AND the temporal-ui client is created with redirectUris: ["http://localhost:8333/*"] and webOrigins: ["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 redirectUris and webOrigins updated 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-a as the slug
  • AND writes COMPOSE_PROJECT_NAME=mc-llm-a to test/.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-slug as the slug regardless of the worktree directory name
  • AND writes COMPOSE_PROJECT_NAME=custom-slug to test/.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/.env so 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/.env already 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-key and test/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-orphans against the stack
  • AND deletes test/.env, test/secrets/, and test/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/.env defines MC_PORT=8090 and the developer exports it before running member-console start --config test/mc-config.yaml
  • THEN the application listens on port 8090 instead of the port: 8081 value in the config file

Scenario: Environment overrides database DSN

  • WHEN test/.env defines MC_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-a and worktree mc-llm-b both bootstrap and bring up their stacks
  • THEN every host port assigned to mc-llm-a differs from every host port assigned to mc-llm-b
  • AND docker ps shows two distinct sets of containers prefixed mc-llm-a-* and mc-llm-b-*
  • AND the Postgres database in mc-llm-a does not contain rows written by the application running against mc-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.sh must be run before docker 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.md or the top-level CLAUDE.md
  • THEN each file contains a reference instructing the reader to consult test/AGENTS.md and run ./test/bootstrap-stack.sh before 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 status after ./test/bootstrap-stack.sh has created test/.env
  • THEN test/.env does not appear as an untracked file
  • AND git check-ignore test/.env reports the file as ignored