# Production-shape harness for local E2E. Multi-tenant.
#
# Reproduces the SaaS tenant topology on localhost using the SAME
# images that ship to production:
#
#   client → cf-proxy (nginx, mimics CF tunnel headers, routes by Host)
#          ├─ Host: harness-tenant-alpha.localhost → tenant-alpha
#          │   ↓ (CP_UPSTREAM_URL=http://cp-stub:9090)
#          │   tenant-alpha (workspace-server/Dockerfile.tenant)
#          │   ↓
#          │   postgres-alpha (per-tenant DB, matches prod)
#          ├─ Host: harness-tenant-beta.localhost  → tenant-beta
#          │   ↓
#          │   tenant-beta + postgres-beta
#          └─ cp-stub + redis (shared infra; CP is Railway-singleton in prod,
#                              redis is shared cluster)
#
# The two-tenant topology catches:
#   - TenantGuard cross-tenant escape (alpha-org token shouldn't see
#     beta-tenant data even with a valid bearer)
#   - cf-proxy Host-header routing correctness
#   - Per-tenant DB isolation (workspaces table, activity_logs)
#   - Concurrent multi-tenant operation (no shared mutable state)
#
# Quickstart (no /etc/hosts edits — see README):
#   cd tests/harness && ./up.sh && ./seed.sh
#   ./replays/peer-discovery-404.sh
#   ./run-all-replays.sh
#
# Env config:
#   GIT_SHA — passed to BOTH tenant builds for /buildinfo verification.
#   CP_STUB_PEERS_MODE — peers failure mode for replay scripts.

services:
  # ─── Shared infra (matches prod: CP is Railway-singleton, redis shared) ───
  redis:
    image: redis:7-alpine
    networks: [harness-net]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 5s
      retries: 10

  cp-stub:
    build:
      context: ./cp-stub
    environment:
      PORT: "9090"
      CP_STUB_PEERS_MODE: "${CP_STUB_PEERS_MODE:-}"
    networks: [harness-net]
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://localhost:9090/healthz || exit 1"]
      interval: 2s
      timeout: 5s
      retries: 10

  # ─── Tenant alpha: postgres + workspace-server ────────────────────────
  postgres-alpha:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: harness
      POSTGRES_PASSWORD: harness
      POSTGRES_DB: molecule
    networks: [harness-net]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U harness"]
      interval: 2s
      timeout: 5s
      retries: 10

  tenant-alpha:
    build:
      context: ../..
      dockerfile: workspace-server/Dockerfile.tenant
      args:
        GIT_SHA: "${GIT_SHA:-harness}"
    depends_on:
      postgres-alpha:
        condition: service_healthy
      redis:
        condition: service_healthy
      cp-stub:
        condition: service_healthy
    environment:
      DATABASE_URL: "postgres://harness:harness@postgres-alpha:5432/molecule?sslmode=disable"
      REDIS_URL: "redis://redis:6379"
      PORT: "8080"
      PLATFORM_URL: "http://tenant-alpha:8080"
      MOLECULE_ENV: "production"
      SECRETS_ENCRYPTION_KEY: "${SECRETS_ENCRYPTION_KEY:?must be set — run via tests/harness/up.sh, which generates one per run}"
      ADMIN_TOKEN: "harness-admin-token-alpha"
      MOLECULE_ORG_ID: "harness-org-alpha"
      CP_UPSTREAM_URL: "http://cp-stub:9090"
      RATE_LIMIT: "1000"
      CANVAS_PROXY_URL: "http://localhost:3000"
      # Memory v2 sidecar (PR #2906) bundles the plugin into the
      # tenant image and starts it before the main server. The plugin
      # runs `CREATE EXTENSION vector` on first boot, which fails on
      # the harness's plain postgres:15-alpine (no pgvector). The
      # harness doesn't exercise memory features, so disable the
      # sidecar via the entrypoint's documented escape hatch.
      MEMORY_PLUGIN_DISABLE: "1"
    networks: [harness-net]
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://localhost:8080/health || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 20

  # ─── Tenant beta: postgres + workspace-server (parallel to alpha) ─────
  postgres-beta:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: harness
      POSTGRES_PASSWORD: harness
      POSTGRES_DB: molecule
    networks: [harness-net]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U harness"]
      interval: 2s
      timeout: 5s
      retries: 10

  tenant-beta:
    build:
      context: ../..
      dockerfile: workspace-server/Dockerfile.tenant
      args:
        GIT_SHA: "${GIT_SHA:-harness}"
    depends_on:
      postgres-beta:
        condition: service_healthy
      redis:
        condition: service_healthy
      cp-stub:
        condition: service_healthy
    environment:
      DATABASE_URL: "postgres://harness:harness@postgres-beta:5432/molecule?sslmode=disable"
      REDIS_URL: "redis://redis:6379"
      PORT: "8080"
      PLATFORM_URL: "http://tenant-beta:8080"
      MOLECULE_ENV: "production"
      SECRETS_ENCRYPTION_KEY: "${SECRETS_ENCRYPTION_KEY:?must be set — run via tests/harness/up.sh, which generates one per run}"
      # Distinct ADMIN_TOKEN — replays use this to verify TenantGuard
      # blocks alpha-token presented at beta's URL.
      ADMIN_TOKEN: "harness-admin-token-beta"
      MOLECULE_ORG_ID: "harness-org-beta"
      CP_UPSTREAM_URL: "http://cp-stub:9090"
      RATE_LIMIT: "1000"
      CANVAS_PROXY_URL: "http://localhost:3000"
      # Memory v2 sidecar (PR #2906) bundles the plugin into the
      # tenant image and starts it before the main server. The plugin
      # runs `CREATE EXTENSION vector` on first boot, which fails on
      # the harness's plain postgres:15-alpine (no pgvector). The
      # harness doesn't exercise memory features, so disable the
      # sidecar via the entrypoint's documented escape hatch.
      MEMORY_PLUGIN_DISABLE: "1"
    networks: [harness-net]
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://localhost:8080/health || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 20

  # ─── cf-proxy: routes by Host to the right tenant container ───────────
  # Production shape: same single CF tunnel front-doors every tenant
  # subdomain — the Host header carries the tenant identity, not the
  # routing destination. Local cf-proxy mirrors this exactly.
  #
  # nginx.conf delivery: built into a custom image via cf-proxy/Dockerfile
  # (a thin nginx:1.27-alpine + COPY). NOT a bind mount and NOT a
  # compose `configs:` block, both of which break under Gitea's
  # act_runner: the runner talks to the OUTER docker daemon over the
  # host socket, and runc resolves bind sources on the outer host
  # filesystem, where `/workspace/.../tests/harness/cf-proxy/nginx.conf`
  # is invisible. Compose `configs:` falls back to bind mounts without
  # swarm, so it hits the same gap. A build context, by contrast, is
  # uploaded to the daemon as a tarball at build time — no bind. See
  # issue #88 item 2.
  cf-proxy:
    build:
      context: ./cf-proxy
      dockerfile: Dockerfile
    depends_on:
      tenant-alpha:
        condition: service_healthy
      tenant-beta:
        condition: service_healthy
    # Bind to 127.0.0.1 only — hardcoded ADMIN_TOKENs make 0.0.0.0
    # exposure unsafe even on a local network.
    ports:
      - "127.0.0.1:8080:8080"
    networks: [harness-net]

networks:
  harness-net:
    name: molecule-harness-net
