hyperdx/scripts/test-e2e.sh
Warren Lee 6e8ddd3736
feat: isolate dev environment for multi-agent worktree support (#1994)
## Summary
- Isolate dev, E2E, and integration test environments so multiple git worktrees can run all three simultaneously without port conflicts
- Each worktree gets a deterministic slot (0-99) with unique port ranges: dev (30100-31199), E2E (20320-21399), CI integration (14320-40098)
- Dev portal dashboard (http://localhost:9900) auto-discovers all running stacks, streams logs, and provides a History tab for past run logs

## Port Isolation

| Environment | Port Range | Project Name |
|---|---|---|
| Dev stack | 30100-31199 | `hdx-dev-<slot>` |
| E2E tests | 20320-21399 | `e2e-<slot>` |
| CI integration | 14320-40098 | `int-<slot>` |

All three can run simultaneously from the same worktree with zero port conflicts.

## Dev Portal Features

**Live tab:**
- Auto-discovers dev, E2E, and integration Docker containers + local services (API, App)
- Groups all environments for the same worktree into a single card
- SSE log streaming with ANSI color rendering, capped at 5000 lines
- Auto-starts in background from `make dev`, `make dev-e2e`, `make dev-int`

**History tab:**
- Logs archived to `~/.config/hyperdx/dev-slots/<slot>/history/` on exit (instead of deleted)
- Each archived run includes `meta.json` with worktree/branch metadata
- Grouped by worktree with collapsible cards, search by worktree/branch
- View any past log file in the same log panel, delete individual runs or clear all
- Custom dark-themed confirm modal (no native browser dialogs)

## What Changed

- **`scripts/dev-env.sh`** — Slot-based port assignments, portal auto-start, log archival on exit
- **`scripts/test-e2e.sh`** — E2E port range (20320-21399), log capture via `tee`, portal auto-start, log archival
- **`scripts/ensure-dev-portal.sh`** — Shared singleton portal launcher (works sourced or executed)
- **`scripts/dev-portal/server.js`** — Discovery for dev/E2E/CI containers, history API (list/read/delete), local service port probing
- **`scripts/dev-portal/index.html`** — Live/History tabs, worktree-grouped cards, search, collapse/expand, custom confirm modal, ANSI color log rendering
- **`docker-compose.dev.yml`** — Parameterized ports/volumes/project name with `hdx.dev.*` labels
- **`packages/app/tests/e2e/docker-compose.yml`** — Updated to new E2E port defaults
- **`Makefile`** — `dev-int`/`dev-e2e` targets with log capture + portal auto-start; `dev-portal-stop`; `dev-clean` stops everything + wipes slot data
- **`.env` files** — Ports use `${VAR:-default}` syntax across dev, E2E, and CI environments
- **`agent_docs/development.md`** — Full documentation for isolation, port tables, E2E/CI port ranges

## How to Use

```bash
# Start dev stack (auto-starts portal)
make dev

# Run E2E tests (auto-starts portal, separate ports)
make dev-e2e FILE=navigation

# Run integration tests (auto-starts portal, separate ports)
make dev-int FILE=alerts

# All three can run simultaneously from the same worktree
# Portal at http://localhost:9900 shows everything

# Stop portal
make dev-portal-stop

# Clean up everything (all stacks + portal + history)
make dev-clean
```

## Dev Portal

<img width="1692" height="944" alt="image" src="https://github.com/user-attachments/assets/6ed388a3-43bc-4552-aa8d-688077b79fb7" />

<img width="1689" height="935" alt="image" src="https://github.com/user-attachments/assets/8677a138-0a40-4746-93ed-3b355c8bd45e" />

## Test Plan
- [x] Run `make dev` — verify services start with slot-assigned ports
- [x] Run `make dev` in a second worktree — verify different ports, no conflicts
- [x] Run `make dev-e2e` and `make dev-int` simultaneously — no port conflicts
- [x] Open http://localhost:9900 — verify all stacks grouped by worktree
- [x] Click a service to view logs — verify ANSI colors render correctly
- [x] Stop a stack — verify logs archived to History tab with correct worktree
- [x] History tab — search, collapse/expand, view archived logs, delete
- [x] `make dev-clean` — stops everything, wipes slot data and history
2026-03-31 18:24:24 +00:00

257 lines
9.2 KiB
Bash
Executable file

#!/bin/bash
# Run E2E tests in full-stack or local mode
# Full-stack mode (default): MongoDB + API + local ClickHouse
# Local mode: Frontend + local ClickHouse (no MongoDB/API)
#
# Usage:
# ./scripts/test-e2e.sh # Run all tests in fullstack mode
# ./scripts/test-e2e.sh --local # Run in local mode (frontend + ClickHouse only)
# ./scripts/test-e2e.sh --keep-running # Keep containers running after tests (fast iteration!)
# ./scripts/test-e2e.sh --ui # Run with Playwright UI
# ./scripts/test-e2e.sh --last-failed # Run only failed tests
# ./scripts/test-e2e.sh --headed # Run with visible browser
# ./scripts/test-e2e.sh --debug # Run in debug mode
# ./scripts/test-e2e.sh --grep "dashboard" # Run tests matching pattern
#
# Development workflow (recommended):
# ./scripts/test-e2e.sh --keep-running --ui # Start containers and open UI
# # Make changes, tests auto-rerun in UI mode
# # When done:
# docker compose -p e2e-<slot> -f packages/app/tests/e2e/docker-compose.yml down -v
#
# All Playwright flags are passed through automatically
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DOCKER_COMPOSE_FILE="$REPO_ROOT/packages/app/tests/e2e/docker-compose.yml"
# ---------------------------------------------------------------------------
# Multi-agent / worktree isolation
# ---------------------------------------------------------------------------
# Compute a deterministic port offset (0-99) from the working directory name
# so that multiple worktrees can run E2E tests in parallel without port
# conflicts. Override HDX_E2E_SLOT manually if you need a specific slot.
#
# Port allocation — E2E gets its own range (20320-21399) so it can run
# simultaneously with CI integration tests (14320-40098) and the dev
# stack (30100-31199). All ports are below the OS ephemeral range
# (32768 Linux, 49152 macOS).
#
# Port mapping (base + slot):
# OpAMP : 20320 + slot (20320-20419)
# ClickHouse HTTP : 20500 + slot (20500-20599)
# ClickHouse Native: 20600 + slot (20600-20699)
# API server : 21000 + slot (21000-21099)
# MongoDB : 21100 + slot (21100-21199)
# App (local) : 21200 + slot (21200-21299)
# App (fullstack) : 21300 + slot (21300-21399)
# ---------------------------------------------------------------------------
export HDX_E2E_SLOT="${HDX_E2E_SLOT:-$(printf '%s' "$(basename "$REPO_ROOT")" | cksum | awk '{print $1 % 100}')}"
export HDX_E2E_OPAMP_PORT="${HDX_E2E_OPAMP_PORT:-$((20320 + HDX_E2E_SLOT))}"
export HDX_E2E_CH_PORT="${HDX_E2E_CH_PORT:-$((20500 + HDX_E2E_SLOT))}"
export HDX_E2E_CH_NATIVE_PORT="${HDX_E2E_CH_NATIVE_PORT:-$((20600 + HDX_E2E_SLOT))}"
export HDX_E2E_API_PORT="${HDX_E2E_API_PORT:-$((21000 + HDX_E2E_SLOT))}"
export HDX_E2E_MONGO_PORT="${HDX_E2E_MONGO_PORT:-$((21100 + HDX_E2E_SLOT))}"
export HDX_E2E_APP_LOCAL_PORT="${HDX_E2E_APP_LOCAL_PORT:-$((21200 + HDX_E2E_SLOT))}"
export HDX_E2E_APP_PORT="${HDX_E2E_APP_PORT:-$((21300 + HDX_E2E_SLOT))}"
export E2E_PROJECT="e2e-${HDX_E2E_SLOT}"
# --- Log capture for dev-portal visibility ---
HDX_E2E_SLOTS_DIR="${HOME}/.config/hyperdx/dev-slots"
HDX_E2E_LOGS_DIR="${HDX_E2E_SLOTS_DIR}/${HDX_E2E_SLOT}/logs-e2e"
mkdir -p "$HDX_E2E_LOGS_DIR"
exec > >(tee "$HDX_E2E_LOGS_DIR/e2e.log") 2>&1
# --- Start dev portal in background if not already running ---
# shellcheck source=./ensure-dev-portal.sh
source "${REPO_ROOT}/scripts/ensure-dev-portal.sh"
echo "Using E2E slot ${HDX_E2E_SLOT} (project=${E2E_PROJECT} ch=${HDX_E2E_CH_PORT} ch-native=${HDX_E2E_CH_NATIVE_PORT} mongo=${HDX_E2E_MONGO_PORT} api=${HDX_E2E_API_PORT} app=${HDX_E2E_APP_PORT} app-local=${HDX_E2E_APP_LOCAL_PORT} opamp=${HDX_E2E_OPAMP_PORT})"
# Configuration constants
readonly MAX_MONGODB_WAIT_ATTEMPTS=15
readonly MONGODB_WAIT_DELAY_SECONDS=1
readonly MAX_CLICKHOUSE_WAIT_ATTEMPTS=30
readonly CLICKHOUSE_WAIT_DELAY_SECONDS=1
# Parse arguments
LOCAL_MODE=false
SKIP_CLEANUP=false
PLAYWRIGHT_FLAGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--local)
LOCAL_MODE=true
shift
;;
--keep-running|--no-cleanup)
SKIP_CLEANUP=true
shift
;;
*)
# Pass any other flags through to Playwright
PLAYWRIGHT_FLAGS+=("$1")
shift
;;
esac
done
cleanup_services() {
echo "Stopping E2E services and removing volumes..."
docker compose -p "$E2E_PROJECT" -f "$DOCKER_COMPOSE_FILE" down -v
# Archive logs to history instead of deleting
if [ -d "$HDX_E2E_LOGS_DIR" ] && [ -n "$(ls -A "$HDX_E2E_LOGS_DIR" 2>/dev/null)" ]; then
_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
_hist="${HDX_E2E_SLOTS_DIR}/${HDX_E2E_SLOT}/history/e2e-${_ts}"
mkdir -p "$_hist"
mv "$HDX_E2E_LOGS_DIR"/* "$_hist/" 2>/dev/null || true
_wt=$(basename "$(git -C "$REPO_ROOT" rev-parse --show-toplevel 2>/dev/null || echo "$REPO_ROOT")")
_br=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
cat > "$_hist/meta.json" <<METAEOF
{"worktree":"${_wt}","branch":"${_br}","worktreePath":"${REPO_ROOT}"}
METAEOF
fi
rm -rf "$HDX_E2E_LOGS_DIR" 2>/dev/null || true
}
check_mongodb_health() {
# Health check script that tests ping, insert, and delete operations
# Note: MongoDB runs on port 27017 inside the container (default)
docker compose -p "$E2E_PROJECT" -f "$DOCKER_COMPOSE_FILE" exec -T db mongosh --quiet --eval "
try {
db.adminCommand('ping');
db.getSiblingDB('test').test.insertOne({_id: 'healthcheck', ts: new Date()});
db.getSiblingDB('test').test.deleteOne({_id: 'healthcheck'});
print('ready');
} catch(e) {
print('not ready: ' + e);
quit(1);
}
" 2>&1
}
check_clickhouse_health() {
# Health check from HOST perspective (not inside container)
# This ensures the port is actually accessible to Playwright
curl -sf "http://localhost:${HDX_E2E_CH_PORT}/ping" >/dev/null 2>&1 || wget --spider -q "http://localhost:${HDX_E2E_CH_PORT}/ping" 2>&1
}
wait_for_clickhouse() {
echo "Waiting for ClickHouse to be ready..."
local attempt=1
while [ $attempt -le $MAX_CLICKHOUSE_WAIT_ATTEMPTS ]; do
if check_clickhouse_health >/dev/null 2>&1; then
echo "ClickHouse is ready"
return 0
fi
if [ $attempt -eq $MAX_CLICKHOUSE_WAIT_ATTEMPTS ]; then
local total_wait=$((MAX_CLICKHOUSE_WAIT_ATTEMPTS * CLICKHOUSE_WAIT_DELAY_SECONDS))
echo "ClickHouse failed to become ready after $total_wait seconds"
echo "Try running: docker compose -p $E2E_PROJECT -f $DOCKER_COMPOSE_FILE logs ch-server"
return 1
fi
echo "Waiting for ClickHouse... ($attempt/$MAX_CLICKHOUSE_WAIT_ATTEMPTS)"
attempt=$((attempt + 1))
sleep $CLICKHOUSE_WAIT_DELAY_SECONDS
done
}
wait_for_mongodb() {
echo "Waiting for MongoDB to be ready..."
local attempt=1
# Verify mongosh is available in the container
if ! docker compose -p "$E2E_PROJECT" -f "$DOCKER_COMPOSE_FILE" exec -T db which mongosh >/dev/null 2>&1; then
echo "ERROR: mongosh not found in MongoDB container"
echo "Container may not be running or using incompatible image"
echo "Try running: docker compose -p $E2E_PROJECT -f $DOCKER_COMPOSE_FILE logs db"
return 1
fi
while [ $attempt -le $MAX_MONGODB_WAIT_ATTEMPTS ]; do
local result
result=$(check_mongodb_health)
if echo "$result" | grep -q "ready"; then
echo "MongoDB is ready and accepting writes"
return 0
fi
if [ $attempt -eq $MAX_MONGODB_WAIT_ATTEMPTS ]; then
local total_wait=$((MAX_MONGODB_WAIT_ATTEMPTS * MONGODB_WAIT_DELAY_SECONDS))
echo "MongoDB failed to become ready after $total_wait seconds"
echo "Last error: $result"
return 1
fi
echo "Waiting for MongoDB... ($attempt/$MAX_MONGODB_WAIT_ATTEMPTS)"
attempt=$((attempt + 1))
sleep $MONGODB_WAIT_DELAY_SECONDS
done
}
# Main execution
setup_cleanup_trap() {
if [ "$SKIP_CLEANUP" = false ]; then
trap cleanup_services EXIT ERR
else
echo "⚠️ Skipping cleanup - containers will remain running"
echo " Use 'docker compose -p $E2E_PROJECT -f $DOCKER_COMPOSE_FILE down -v' to stop them manually"
fi
}
setup_clickhouse() {
echo "Starting ClickHouse..."
docker compose -p "$E2E_PROJECT" -f "$DOCKER_COMPOSE_FILE" up -d ch-server
if ! wait_for_clickhouse; then
exit 1
fi
# Note: ClickHouse seeding is handled by Playwright global setup
# - Fullstack mode: global-setup-fullstack.ts
# - Local mode: global-setup-local.ts
}
run_tests() {
cd "$REPO_ROOT/packages/app"
if [ "$LOCAL_MODE" = true ]; then
echo "Running tests in local mode (frontend + ClickHouse)..."
yarn test:e2e --local "${PLAYWRIGHT_FLAGS[@]}"
else
echo "Running tests in full-stack mode (MongoDB + API + ClickHouse)..."
yarn test:e2e "${PLAYWRIGHT_FLAGS[@]}"
fi
}
# Set up cleanup trap
setup_cleanup_trap
# Clean up E2E Next.js build directory to avoid stale lock/cache issues
rm -rf "$REPO_ROOT/packages/app/.next-e2e" 2>/dev/null || true
# Always start and seed ClickHouse (shared by both modes)
setup_clickhouse
# Conditionally start MongoDB for full-stack mode
if [ "$LOCAL_MODE" = false ]; then
echo "Starting MongoDB for full-stack mode..."
docker compose -p "$E2E_PROJECT" -f "$DOCKER_COMPOSE_FILE" up -d db
if ! wait_for_mongodb; then
exit 1
fi
fi
# Run tests
run_tests