#!/usr/bin/env bash # Local stack smoke test for Stage 6. # Brings the stack up, hits /health /ready /metrics, ingests sample heartbeat + events, # verifies the API exposes agent / check / incident state. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$HERE/.." && pwd)" TOKEN="${MONLET_AUTH_TOKEN:-monlet-ui-dev-token}" AGENT_KEY="${MONLET_AGENT_KEY:-monlet-agent-dev-key-0000000000000000}" POSTGRES_HOST_PORT="${MONLET_POSTGRES_HOST_PORT:-15432}" SERVER_HOST_PORT="${MONLET_SERVER_HOST_PORT:-18000}" WEB_HOST_PORT="${MONLET_WEB_HOST_PORT:-13000}" # Export so every `docker compose` call (up/exec/down) sees the UI token env, # including the cleanup trap. export MONLET_AUTH_TOKEN="$TOKEN" export MONLET_POSTGRES_HOST_PORT="$POSTGRES_HOST_PORT" export MONLET_SERVER_HOST_PORT="$SERVER_HOST_PORT" export MONLET_WEB_HOST_PORT="$WEB_HOST_PORT" BASE="${MONLET_API_BASE_URL:-http://127.0.0.1:$SERVER_HOST_PORT}" # PH-review: dedicated project name so smoke does not collide with the # showcase/dev stack (orphan containers, shared postgres volume with a stale # baseline schema, etc.). COMPOSE=(docker compose -p monlet-smoke -f "$HERE/docker-compose.yml") log() { printf "\033[1;36m[smoke]\033[0m %s\n" "$*"; } fail() { printf "\033[1;31m[smoke]\033[0m %s\n" "$*" >&2; exit 1; } cleanup() { if [[ "${SMOKE_KEEP:-0}" != "1" ]]; then log "tearing down stack" "${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true fi } trap cleanup EXIT log "pre-clean previous smoke stack (if any)" "${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true log "starting stack (phase 1: postgres+server)" "${COMPOSE[@]}" up -d --build postgres server log "waiting for /api/v1/ready" for i in $(seq 1 60); do if curl -sf "$BASE/api/v1/ready" >/dev/null; then break; fi sleep 2 [[ "$i" == "60" ]] && fail "server did not become ready" done log "GET /api/v1/health" curl -sf "$BASE/api/v1/health" | grep -q '"status":"ok"' || fail "health wrong body" log "GET /metrics" curl -sf "$BASE/metrics" | grep -q '^monlet_server_' || fail "metrics missing" log "starting web (phase 2)" "${COMPOSE[@]}" up -d --build web AGENT_AUTH=(-H "Authorization: Bearer $AGENT_KEY" -H "Content-Type: application/json") UI_AUTH=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json") log "POST heartbeat (examples/heartbeat.json)" curl -sf "${AGENT_AUTH[@]}" -X POST "$BASE/api/v1/heartbeat" --data @"$ROOT/examples/heartbeat.json" \ | grep -q '"accepted":true' || fail "heartbeat not accepted" log "accept pending agent" curl -sf "${UI_AUTH[@]}" -X POST "$BASE/api/v1/agents/host-01.prod/accept" \ | grep -q '"count":1' || fail "agent not accepted" log "POST events (examples/event-batch.json)" curl -sf "${AGENT_AUTH[@]}" -X POST "$BASE/api/v1/events" --data @"$ROOT/examples/event-batch.json" \ | grep -q '"accepted":' || fail "event batch not accepted" log "GET /agents has host-01.prod" curl -sf "${UI_AUTH[@]}" "$BASE/api/v1/agents" | grep -q '"agent_id":"host-01.prod"' \ || fail "host-01.prod missing" log "GET /checks not empty" curl -sf "${UI_AUTH[@]}" "$BASE/api/v1/checks" | grep -q '"items":\[{' \ || fail "checks empty" log "GET /incidents not empty (event-batch.json contains a critical event)" curl -sf "${UI_AUTH[@]}" "$BASE/api/v1/incidents?state=open" | grep -q '"items":\[{' \ || fail "no open incident" log "GET /notifiers/outbox not empty" curl -sf "${UI_AUTH[@]}" "$BASE/api/v1/notifiers/outbox" | grep -q '"items":\[{' \ || fail "outbox empty" WEB_BASE="${MONLET_WEB_BASE_URL:-http://127.0.0.1:$WEB_HOST_PORT}" log "waiting for web $WEB_BASE" for i in $(seq 1 60); do if curl -sf -o /dev/null "$WEB_BASE/"; then break; fi sleep 2 [[ "$i" == "60" ]] && fail "web did not become ready" done log "GET / (web root follows redirect to /agents)" curl -sfL "$WEB_BASE/" | grep -q "host-01.prod" || fail "web root did not render agents list" log "GET /agents (web list)" curl -sf "$WEB_BASE/agents" | grep -q "host-01.prod" || fail "web /agents missing host-01.prod" log "ALL SMOKE TESTS PASSED"