#!/usr/bin/env bash set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" project_name="${MINEROPS_DEMO_TEST_PROJECT:-minerops-demo-test-$$}" dev_port="${MINEROPS_DEMO_TEST_PORT:-$((21000 + ($$ % 10000)))}" base_url="http://127.0.0.1:${dev_port}" tmp_prefix="/tmp/${project_name}" admin_cookie="${tmp_prefix}-admin.cookies" compose=( docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml ) cleanup() { status=$? if (( status != 0 )); then echo echo "Development demo test failed. Recent service logs:" "${compose[@]}" logs --no-color --tail 160 || true fi rm -f "${tmp_prefix}"-* "${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true exit "${status}" } trap cleanup EXIT status_for() { local path="$1" curl --silent --output /dev/null --write-out '%{http_code}' \ -b "${admin_cookie}" -c "${admin_cookie}" "${base_url}${path}" } printf '%s\n' 'APP_ENV=production' > "${tmp_prefix}-production.env" if MINEROPS_DEMO_ENV_FILE="${tmp_prefix}-production.env" \ ./scripts/seed-development-demo.sh >"${tmp_prefix}-safety.out" 2>&1; then echo "Demo seed accepted a production environment." >&2 exit 1 fi grep -q 'APP_ENV must be exactly development' "${tmp_prefix}-safety.out" echo "Starting disposable MinerOps data services..." MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach postgres redis for attempt in $(seq 1 180); do postgres_ready=false redis_ready=false if "${compose[@]}" exec -T postgres sh -ec \ 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atqc "SELECT 1"' \ 2>/dev/null | grep -qx 1; then postgres_ready=true fi if "${compose[@]}" exec -T redis redis-cli ping 2>/dev/null | grep -qx PONG; then redis_ready=true fi if [[ "${postgres_ready}" == "true" && "${redis_ready}" == "true" ]]; then break fi (( attempt < 180 )) || { echo "Disposable data services did not become ready." >&2; exit 1; } sleep 1 done echo "Building and starting the disposable backend..." MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach --build --no-deps backend backend_container="$("${compose[@]}" ps -q backend)" for attempt in $(seq 1 180); do backend_health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${backend_container}")" [[ "${backend_health}" == "healthy" ]] && break [[ "${backend_health}" != "exited" && "${backend_health}" != "dead" ]] || { echo "Disposable backend exited." >&2; exit 1; } (( attempt < 180 )) || { echo "Disposable backend did not become ready." >&2; exit 1; } sleep 1 done echo "Starting the disposable gateway..." MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach --no-deps gateway for attempt in $(seq 1 90); do if curl --fail --silent "${base_url}/api/health" >"${tmp_prefix}-health.json"; then break fi (( attempt < 90 )) || { echo "Disposable gateway did not become ready." >&2; exit 1; } sleep 1 done echo "Creating disposable development administrator..." curl --fail --silent -c "${admin_cookie}" \ -H 'Content-Type: application/json' \ --data '{"username":"admin","display_name":"Test Administrator","email":"admin@example.com","password":"MinerOpsTest!123"}' \ "${base_url}/api/auth/setup-admin" >"${tmp_prefix}-setup.json" grep -q '"role":"Admin"' "${tmp_prefix}-setup.json" echo "Seeding and reapplying the deterministic fixture..." MINEROPS_DEMO_PROJECT="${project_name}" ./scripts/seed-development-demo.sh MINEROPS_DEMO_PROJECT="${project_name}" ./scripts/seed-development-demo.sh MINEROPS_DEMO_PROJECT="${project_name}" ./scripts/seed-development-demo.sh --reseed counts="$( "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -At -F "|" -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL' SELECT count(*) FILTER (WHERE asset_tag LIKE 'DEMO-MIN-%'), (SELECT count(*) FROM location_positions WHERE source_file = 'MinerOps demo seed'), (SELECT count(*) FROM location_positions WHERE source_file = 'MinerOps demo seed' AND miner_id IS NULL), (SELECT count(*) FROM repairs WHERE id::text LIKE 'd4000000-0000-4000-8000-%'), (SELECT count(*) FROM repairs WHERE id::text LIKE 'd4000000-0000-4000-8000-%' AND status IN ('OPEN', 'IN_PROGRESS', 'WAITING_FOR_PARTS', 'READY_FOR_TEST')), (SELECT count(*) FROM repair_events WHERE id::text LIKE 'd5000000-0000-4000-8000-%'), (SELECT count(*) FROM miner_status_snapshots WHERE id::text LIKE 'd3000000-0000-4000-8000-%') FROM miners; SQL )" [[ "${counts}" == "30|50|23|4|3|8|23" ]] echo "Checking live authenticated pages and APIs..." for path in /dashboard /assets/miners /monitoring/map /operations/repairs; do [[ "$(status_for "${path}")" == "200" ]] done curl --fail --silent -b "${admin_cookie}" "${base_url}/monitoring/map" >"${tmp_prefix}-map.html" curl --fail --silent -b "${admin_cookie}" "${base_url}/assets/minerops-mine-map.js" >"${tmp_prefix}-map.js" curl --fail --silent -b "${admin_cookie}" "${base_url}/api/location/map" >"${tmp_prefix}-map.json" curl --fail --silent -b "${admin_cookie}" "${base_url}/api/inventory/miners?limit=100" >"${tmp_prefix}-miners.json" curl --fail --silent -b "${admin_cookie}" "${base_url}/api/repairs/queue" >"${tmp_prefix}-repairs.json" for id in mapSite mapRoom mapLayer mapPermissionNote mapViewport mapInspector; do grep -q "id=\"${id}\"" "${tmp_prefix}-map.html" done for layer in temperature hashrate power repair; do grep -q "\"${layer}\"" "${tmp_prefix}-map.js" done ! grep -q "Cannot set properties of null" "${tmp_prefix}-map.html" python3 - "${tmp_prefix}-map.json" "${tmp_prefix}-miners.json" "${tmp_prefix}-repairs.json" <<'PY' import json import sys with open(sys.argv[1], encoding="utf-8") as handle: mine_map = json.load(handle) with open(sys.argv[2], encoding="utf-8") as handle: miners = json.load(handle) with open(sys.argv[3], encoding="utf-8") as handle: repairs = json.load(handle) assert mine_map["meta"]["miner_details_included"] is True assert mine_map["meta"]["position_count"] == 50 assert len(mine_map["data"]["sites"]) >= 1 assert {room["name"] for room in mine_map["data"]["rooms"]} >= {"Demo Unit A", "Demo Unit B"} positions = mine_map["data"]["positions"] assert {position["rack_name"] for position in positions} >= {"A1", "A2", "A3", "B1", "B2"} assert sum(position["occupied"] for position in positions) == 27 assert sum(not position["occupied"] for position in positions) == 23 assert any(position["active_repair"] for position in positions) assert any(position["occupied"] and position["observed_at"] is None for position in positions) assert any(position["hashrate_ths"] == 0 for position in positions) assert len({position["model"] for position in positions if position["model"]}) == 6 assert len({position["chip_temp_max_c"] for position in positions if position["chip_temp_max_c"] is not None}) >= 5 assert len(miners["data"]) == 30 assert any(miner["asset_id"] == "DEMO-MIN-001" for miner in miners["data"]) assert repairs["data"]["summary"]["total"] == 3 assert {item["latest_event_status"] for item in repairs["data"]["items"]} == {"open", "waiting_parts", "testing"} PY for query in \ 'query=DEMO-MIN-001' \ 'query=DEMO-SERIAL-001' \ 'query=02%3A00%3A00%3A00%3A00%3A01' \ 'query=M60S'; do response="$(curl --fail --silent -b "${admin_cookie}" "${base_url}/api/inventory/miners?${query}&limit=100")" [[ "${response}" == *'"returned":'* ]] [[ "${response}" != *'"returned":0'* ]] done echo "Development demo seed and live UI/API smoke checks passed."