Put the fedwiki chain (init, render, farm, caddy) behind a fedwiki compose profile symmetric with discourse's; the default composition is neither, selected via COMPOSE_PROFILES in test/.env, so a default stack no longer binds host 443. Guard every script and walkthrough on service presence: shared skipUnlessIntegrationEndpointReachable helper, seed-stack presence checks (also repairing its unsourced .env and container-native render invocation), generic root-owned testdata reclaim in teardown, discourse coverage in verify-stack-isolation, and fedwiki's 8090 base in the port probe. Update stack docs and finalize status bookkeeping for all three changes; archives the test-stack-integration-profiles change.
21 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, and Temporal UI to 8233
- AND the Keycloak hostname is
keycloak.localhost - AND FedWiki binds to host port 80 and Caddy to 443 only when the
fedwikiprofile is selected
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 that the selected profiles compose 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: Integration services are profile-gated symmetrically
Each test/compose.yaml service that exists solely to back an integration SHALL
declare a compose profile named after that integration, so no integration
service starts unless its profile is selected. The FedWiki chain
(fedwiki-init, fedwiki-render, fedwiki) and the Caddy TLS proxy that
fronts the wiki farm SHALL belong to the fedwiki profile; the Discourse chain
SHALL keep its existing discourse profile. No un-profiled service SHALL
declare a dependency on a profiled one.
Scenario: Default composition starts no integration services
- WHEN a developer brings the stack up with no compose profiles selected
- THEN Postgres, Valkey, Keycloak, the Keycloak seed, Temporal, and the Temporal UI start
- AND no
fedwiki-init,fedwiki-render,fedwiki,caddy, or Discourse container is created - AND no process binds host port
443
Scenario: Selecting the fedwiki profile starts the whole wiki chain
- WHEN the stack is brought up with the
fedwikiprofile selected - THEN
fedwiki-initandfedwiki-renderrun to completion,fedwikistarts, andcaddystarts fronting it - AND the core services are unaffected
Scenario: Profiles are independently selectable
- WHEN the stack is brought up with only the
discourseprofile selected - THEN the Discourse services start and no FedWiki or Caddy container is created
Requirement: Bootstrap writes an explicit demo-composition knob
The test/bootstrap-stack.sh script SHALL write a COMPOSE_PROFILES variable
into test/.env as the single, documented knob deciding which integrations a
stack composes. The shipped default SHALL be empty (core stack only). The knob
SHALL be settable at generation time from the environment, and SHALL be
editable afterwards without regenerating the rest of test/.env. Whether an
integration is composed SHALL be independent of whether the repository
supports it: a demo deployment turns integrations on through this knob alone.
Scenario: Default bootstrap composes the core stack only
- WHEN the bootstrap script generates
test/.envwith noCOMPOSE_PROFILESset in its environment - THEN
test/.envcontainsCOMPOSE_PROFILES=with an empty value - AND a subsequent
docker compose up -dstarts no integration service
Scenario: Bootstrap honours a generation-time override
- WHEN the bootstrap script is invoked with
COMPOSE_PROFILES=fedwikiin its environment - THEN the generated
test/.envrecordsCOMPOSE_PROFILES=fedwiki
Scenario: Bootstrap reports the active composition
- WHEN the bootstrap script prints its summary, whether on first run or on an idempotent re-run
- THEN the summary states which integration profiles are active, naming "none (core stack only)" when the knob is empty
- AND names the profiles a reader can add to the knob to compose more
Requirement: Integration app-configuration is opt-in in the generated env
The test/.env that bootstrap-stack.sh generates SHALL leave the MC_*
configuration for every profile-gated integration commented out, so the
application boots without that integration by default. test/mc-config.yaml
SHALL NOT re-enable a profile-gated integration behind the knob's back: it may
carry only inert knobs that have no effect while the integration's required
configuration group is unset.
Scenario: FedWiki app wiring is commented out by default
- WHEN the bootstrap script generates
test/.env - THEN the FedWiki farm API URL and admin token appear as commented-out lines carrying the worktree's allocated FedWiki port
- AND the accompanying comment tells the reader to uncomment them together
with the
fedwikiprofile
Scenario: The application boots with no integration configured
- WHEN the application is started against the generated
test/.envandtest/mc-config.yamlwith no integration lines uncommented - THEN boot validation passes and the server serves its dashboard
- AND no FedWiki or Discourse client is constructed
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, the compose-profile composition knob, Keycloak hostname, member-console base URL, temporal-ui base URL, and the test-database DSNs, 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, the active integration profiles, and the member-console, Keycloak, and Temporal UI URLs that the developer should use
- AND presents the FedWiki and Discourse URLs as conditional on their profiles
Scenario: Bootstrap writes the test-database DSNs
- WHEN the bootstrap script generates
test/.env - THEN the file SHALL contain a DSN for the package-test database, a DSN for the in-process e2e database, and a maintenance DSN used to drop and recreate them
- AND all three SHALL address the same worktree-allocated Postgres host
port as
MC_DB_DSN - AND neither test DSN SHALL name the application database
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: Stack scripts tolerate absent integration services
No script under test/ SHALL assume a profile-gated service exists. Seed and
demo paths SHALL detect service presence and skip that integration's steps with
an explanatory message rather than failing. Teardown SHALL remove containers,
volumes, and root-owned runtime data for gated services without naming any
service-specific path.
Scenario: Seeding skips FedWiki when the profile is off
- WHEN
test/seed-stack.shruns against a stack composed without thefedwikiprofile - THEN the Keycloak realm is seeded
- AND the FedWiki identity-fixture and restart steps are skipped with a message naming the knob that would enable them
- AND the script exits with status 0
Scenario: Teardown removes gated services and their root-owned data
- WHEN
test/teardown-stack.shruns after a stack that had integration profiles active - THEN the gated containers and their volumes are removed along with the core stack's
- AND root-owned runtime data under
test/testdata/is removed regardless of which integration wrote it, without the script enumerating per-service paths
Scenario: Port probing covers every integration's base port
- WHEN the bootstrap script tests whether a candidate slot is free
- THEN it probes the base host port of every service the stack can compose, including FedWiki's and Discourse's
Requirement: Walkthroughs skip when an integration service is unreachable
Browser walkthroughs that touch an integration SHALL probe that integration's effective, app-configured endpoint and skip when it does not answer, so the suite passes against a stack composed without that integration. The probe SHALL read the endpoint from the running application rather than hardcoding a port, and SHALL distinguish "not configured" (let the walkthrough's own assertions speak) from "configured but unreachable" (skip).
Scenario: Integration-settings walkthrough skips a down wiki farm
- WHEN the integration-settings walkthrough runs against an application configured with a FedWiki farm URL that does not answer
- THEN the walkthrough skips with a message naming the unreachable URL and the profile that would start it
Scenario: Test suite passes with no integration services running
- WHEN the full Go test suite runs against a stack composed with no integration profiles
- THEN every package passes, with integration-dependent walkthroughs reported as skipped rather than failed
Requirement: Stack isolation verification covers every profiled integration
test/verify-stack-isolation.sh SHALL assert that the Keycloak seed templates
per-stack URLs for every OIDC client it seeds on behalf of an integration, not
only the clients that predate profile gating.
Scenario: Discourse client redirect URIs are asserted
- WHEN the isolation verification script runs against a forced non-default slot
- THEN it asserts the seeded
discourseclient's redirect URIs and web origins reflect the slot's templated Discourse host and port
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. That documentation SHALL describe the composition knob and state that the default stack composes no integration.
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
- AND documents the
COMPOSE_PROFILESknob, the default core-only composition, and what an agent must do before exercising a FedWiki or Discourse flow
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
Requirement: Test-stack credentials are unmistakably test-only
Every credential seeded or committed for the test/ stack SHALL be recognizable as
test-only and non-production. Files that carry secrets and support comments SHALL
include a prominent "test-only, do not use in production" banner, and the generated
test/.env SHALL carry the same banner. For secret-bearing formats that do not
support comments, or for values most likely to be copied out of context, the value
itself SHALL be self-labeling where the secret is single-consumer and safe to
re-value. No file containing real cryptographic material (private keys,
symmetric secrets, or password hashes) SHALL be tracked in git unless the compose
stack actually consumes it.
Scenario: Committed secret-bearing files are labeled test-only
- WHEN a contributor opens a
test/file that seeds or defines credentials (for examplemc-config.yaml,compose.yaml,seed/keycloak/seed-keycloak.sh, orseed/fedwiki/config.json.tpl) - THEN the file — or, for comment-free formats, the credential values themselves — SHALL make clear the credentials are test-only and must not be reused in a real deployment
Scenario: Generated .env is labeled test-only
- WHEN
bootstrap-stack.shwritestest/.env - THEN the file SHALL include a banner stating its credentials are throwaway test-only values
Scenario: No unused real cryptographic material is tracked
- WHEN the repository is inspected for committed secrets
- THEN it SHALL NOT track a Keycloak realm export (or similar artifact) containing real private keys, symmetric secrets, or password hashes that the compose stack never imports