Files
cgalo5758 d45a51d2e8 Profile-gate FedWiki and Discourse in the test stack
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.
2026-08-01 04:14:05 -05:00

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 -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, 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 fedwiki profile is selected

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 that the selected profiles compose 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: 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 fedwiki profile selected
  • THEN fedwiki-init and fedwiki-render run to completion, fedwiki starts, and caddy starts fronting it
  • AND the core services are unaffected

Scenario: Profiles are independently selectable

  • WHEN the stack is brought up with only the discourse profile 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/.env with no COMPOSE_PROFILES set in its environment
  • THEN test/.env contains COMPOSE_PROFILES= with an empty value
  • AND a subsequent docker compose up -d starts no integration service

Scenario: Bootstrap honours a generation-time override

  • WHEN the bootstrap script is invoked with COMPOSE_PROFILES=fedwiki in its environment
  • THEN the generated test/.env records COMPOSE_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 fedwiki profile

Scenario: The application boots with no integration configured

  • WHEN the application is started against the generated test/.env and test/mc-config.yaml with 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_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, 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-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, 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-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: 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.sh runs against a stack composed without the fedwiki profile
  • 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.sh runs 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 discourse client'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/.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. 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.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
  • AND documents the COMPOSE_PROFILES knob, 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.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

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 example mc-config.yaml, compose.yaml, seed/keycloak/seed-keycloak.sh, or seed/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.sh writes test/.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