Files
vigilcare-clinical/scripts/run-phase24-verification.sh
T
voltsrage 2a3ef62a7d
CI / frontend (push) Failing after 57s
CI / backend (push) Failing after 6m27s
Add deployment
2026-08-05 00:26:20 +08:00

298 lines
9.9 KiB
Bash

#!/usr/bin/env bash
# Phase 24 — ward-outage-reconnect-01 end-to-end verification.
#
# Runs Phase A (central baseline), Phase B (gateway replay while central down),
# and Phase C (central restart + sync verification).
#
# Optional env:
# SKIP_DOCKER=1 — assume docker stack already up
# SKIP_PHASE_A=1 — skip central replay; use newest ICU encounter on gateway
# CENTRAL_URL — default http://localhost:5270
# GATEWAY_URL — default http://localhost:5081
# SCENARIO_SPEED — simulator speed (default 0 = instant)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
CENTRAL="${CENTRAL_URL:-http://localhost:5270}"
GW="${GATEWAY_URL:-http://localhost:5081}"
GW_ID="${GATEWAY_ID:-22222222-2222-2222-2222-222222222222}"
SCENARIO="${ROOT}/VigilCare.Simulator/Scenarios/List/ward-outage-reconnect-01.json"
SPEED="${SCENARIO_SPEED:-0}"
LOG_DIR="${TMPDIR:-/tmp}/vigilcare-phase24-$$"
CENTRAL_PID=""
WE_STARTED_CENTRAL=0
STOPPED_CENTRAL=0
mkdir -p "$LOG_DIR"
log() { echo "==> $*"; }
die() { echo "ERROR: $*" >&2; exit 1; }
wait_for_url() {
local url="$1" max="${2:-60}" i
for i in $(seq 1 "$max"); do
if curl -sf "$url" >/dev/null 2>&1; then
return 0
fi
sleep 2
done
return 1
}
central_login() {
local user="$1" pass="$2"
curl -sf -X POST "${CENTRAL}/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${user}\",\"password\":\"${pass}\"}" \
| jq -r '.data.accessToken'
}
ensure_central_running() {
if curl -sf "${CENTRAL}/health/ready" >/dev/null 2>&1; then
log "Central API already running at $CENTRAL"
return
fi
log "Starting central API (log: $LOG_DIR/central.log)"
dotnet run --project VigilCareClinicalAPI --launch-profile http \
>"$LOG_DIR/central.log" 2>&1 &
CENTRAL_PID=$!
WE_STARTED_CENTRAL=1
wait_for_url "${CENTRAL}/health/ready" 90 \
|| die "Central API did not become ready — see $LOG_DIR/central.log"
log "Central API ready"
}
stop_central() {
if [[ "$STOPPED_CENTRAL" -eq 1 ]]; then
return
fi
log "Stopping central API to simulate outage"
if [[ -n "$CENTRAL_PID" ]] && kill -0 "$CENTRAL_PID" 2>/dev/null; then
kill "$CENTRAL_PID" 2>/dev/null || true
wait "$CENTRAL_PID" 2>/dev/null || true
CENTRAL_PID=""
else
pkill -f "VigilCareClinicalAPI" 2>/dev/null || true
fi
for _ in $(seq 1 20); do
curl -sf "${CENTRAL}/health/ready" >/dev/null 2>&1 || break
sleep 1
done
STOPPED_CENTRAL=1
}
restart_central() {
if curl -sf "${CENTRAL}/health/ready" >/dev/null 2>&1; then
log "Central API already running"
STOPPED_CENTRAL=0
return
fi
log "Restarting central API"
dotnet run --project VigilCareClinicalAPI --launch-profile http \
>>"$LOG_DIR/central.log" 2>&1 &
CENTRAL_PID=$!
WE_STARTED_CENTRAL=1
wait_for_url "${CENTRAL}/health/ready" 90 \
|| die "Central API failed to restart — see $LOG_DIR/central.log"
STOPPED_CENTRAL=0
log "Central API back online"
}
configure_gateway_sync() {
local sync_jwt="$1"
log "Configuring gateway encounter sync (GATEWAY_SYNC_JWT)"
GATEWAY_SYNC_JWT="$sync_jwt" docker compose --profile ward-gateway up -d --force-recreate ward-gateway-api
wait_for_url "${GW}/health/ready" 60 \
|| die "Gateway not ready after restart"
}
wait_for_encounter_on_gateway() {
local encounter_id="$1" jwt="$2" max_attempts="${3:-60}"
local i
for i in $(seq 1 "$max_attempts"); do
if curl -sf "${GW}/api/v1/encounters/${encounter_id}" \
-H "Authorization: Bearer ${jwt}" >/dev/null 2>&1; then
log "Encounter on gateway after ${i} attempt(s)"
return 0
fi
sleep 3
done
return 1
}
wait_for_gateway_encounter() {
local jwt="$1" encounter_id="" i
for i in $(seq 1 45); do
encounter_id=$(curl -sf "${GW}/api/v1/encounters?status=ACTIVE&department=ICU" \
-H "Authorization: Bearer ${jwt}" \
| jq -r '.data.items[0].encounterId // empty')
if [[ -n "$encounter_id" ]]; then
echo "$encounter_id"
return 0
fi
sleep 4
done
return 1
}
parse_encounter_from_replay() {
local log_file="$1"
grep -oE 'Encounter opened: [0-9a-f-]{36}' "$log_file" \
| head -1 \
| sed 's/Encounter opened: //'
}
cleanup() {
if [[ "${KEEP_CENTRAL_DOWN:-0}" == "1" ]]; then
return
fi
if [[ "$STOPPED_CENTRAL" -eq 1 ]]; then
restart_central || true
fi
}
trap cleanup EXIT
command -v jq >/dev/null 2>&1 || die "jq is required"
command -v python3 >/dev/null 2>&1 || die "python3 is required (for mint-gateway-jwt.sh)"
log "Phase 24 verification — ward-outage-reconnect-01"
if [[ "${SKIP_DOCKER:-0}" != "1" ]]; then
log "Starting docker infrastructure"
docker compose up -d
log "Building ward gateway API image"
docker compose --profile ward-gateway build ward-gateway-api
log "Starting ward gateway profile"
docker compose --profile ward-gateway up -d
else
log "SKIP_DOCKER=1 — assuming stack is already up"
log "Rebuilding ward gateway API image (picks up local code changes)"
docker compose --profile ward-gateway build ward-gateway-api
fi
wait_for_url "${GW}/health/ready" 60 \
|| die "Gateway not ready at $GW — run: docker compose --profile ward-gateway up -d"
ensure_central_running
PHYSICIAN_JWT=$(central_login "physician.demo" "DemoPhysician1!")
ADMIN_JWT=$(central_login "admin.demo" "DemoAdmin1!")
GATEWAY_JWT=$("${ROOT}/scripts/mint-gateway-jwt.sh" nurse.demo NURSE)
export GATEWAY_JWT
configure_gateway_sync "$PHYSICIAN_JWT"
ENCOUNTER_ID=""
if [[ "${SKIP_PHASE_A:-0}" != "1" ]]; then
log "Phase A — baseline replay on central"
PHASE_A_LOG="$LOG_DIR/phase-a.log"
dotnet run --project VigilCare.Simulator -- replay "$SCENARIO" \
--speed "$SPEED" --base-url "$CENTRAL" \
2>&1 | tee "$PHASE_A_LOG"
ENCOUNTER_ID=$(parse_encounter_from_replay "$PHASE_A_LOG")
[[ -n "$ENCOUNTER_ID" ]] || die "Phase A did not report an encounter id"
log "Phase A encounter: $ENCOUNTER_ID"
configure_gateway_sync "$PHYSICIAN_JWT"
wait_for_encounter_on_gateway "$ENCOUNTER_ID" "$GATEWAY_JWT" 60 \
|| die "Encounter $ENCOUNTER_ID not replicated to gateway after Phase A"
fi
if [[ -z "$ENCOUNTER_ID" ]]; then
log "Waiting for ICU encounter on gateway replica"
ENCOUNTER_ID=$(wait_for_gateway_encounter "$GATEWAY_JWT") \
|| die "No ACTIVE ICU encounter on gateway — run Phase A or set GATEWAY_SYNC_JWT"
fi
log "Using encounter $ENCOUNTER_ID"
wait_for_encounter_on_gateway "$ENCOUNTER_ID" "$GATEWAY_JWT" 10 \
|| die "Encounter $ENCOUNTER_ID not available on gateway"
stop_central
log "Waiting for gateway to detect central unreachable (~35s)"
sleep 35
log "Phase B — replay against gateway while central is down"
dotnet run --project VigilCare.Simulator -- replay "$SCENARIO" \
--gateway --encounter-id "$ENCOUNTER_ID" --gateway-token "$GATEWAY_JWT" \
--speed "$SPEED"
log "Verify critical potassium alert acknowledged locally by RN-Wu"
GW_ALERTS=$(curl -sf "${GW}/api/v1/encounters/${ENCOUNTER_ID}/alerts" \
-H "Authorization: Bearer ${GATEWAY_JWT}")
echo "$GW_ALERTS" | jq -e \
'.data.items[] | select(.alertType | ascii_downcase | contains("critical") and contains("potassium")) | (.status == "ACKNOWLEDGED" or .status == "Acknowledged")' \
>/dev/null \
|| die "Expected ACKNOWLEDGED critical potassium alert on gateway"
echo "$GW_ALERTS" | jq -e \
'.data.items[] | select(.alertType | ascii_downcase | contains("critical") and contains("potassium")) | .acknowledgedBy == "RN-Wu"' \
>/dev/null \
|| die "Expected acknowledgedBy RN-Wu on gateway"
BUFFER_DEPTH=$(docker exec "$(docker ps -qf name=ward-gateway-db)" \
psql -U postgres -d vigilcare_ward -tAc \
"SELECT COUNT(*) FROM buffered_sync_items WHERE NOT synced;")
log "Gateway buffer depth (unsynced items): $BUFFER_DEPTH"
[[ "$BUFFER_DEPTH" -gt 0 ]] || die "Expected buffered sync items on gateway after Phase B"
log "Phase C — restart central and wait for sync"
restart_central
log "Waiting up to 180s for gateway-synced alert on central"
SYNCED_ALERT=0
for _ in $(seq 1 36); do
CENTRAL_ALERTS=$(curl -sf "${CENTRAL}/api/v1/encounters/${ENCOUNTER_ID}/alerts" \
-H "Authorization: Bearer ${PHYSICIAN_JWT}" || echo "")
if echo "$CENTRAL_ALERTS" | jq -e \
'.data.items[]? | select(.alertType | ascii_downcase | contains("critical") and contains("potassium")) | select(.syncedFromGateway == true) | .acknowledgedBy == "RN-Wu"' \
>/dev/null 2>&1; then
SYNCED_ALERT=1
break
fi
sleep 5
done
[[ "$SYNCED_ALERT" -eq 1 ]] || die "Timed out waiting for gateway-synced RN-Wu ack on central"
BUFFER=$(curl -sf "${CENTRAL}/api/v1/operations/gateways" \
-H "Authorization: Bearer ${ADMIN_JWT}" \
| jq -r ".data[] | select(.id == \"${GW_ID}\") | .reportedBufferDepth // empty" \
| head -1)
log "Gateway reported buffer depth: ${BUFFER:-unknown}"
log "Verify central received alert acknowledgement"
CENTRAL_ALERTS=$(curl -sf "${CENTRAL}/api/v1/encounters/${ENCOUNTER_ID}/alerts" \
-H "Authorization: Bearer ${PHYSICIAN_JWT}")
echo "$CENTRAL_ALERTS" | jq -e \
'.data.items[] | select(.alertType | ascii_downcase | contains("critical") and contains("potassium")) | select(.syncedFromGateway == true)' \
>/dev/null \
|| die "Critical potassium alert not synced from gateway to central"
ACK_BY=$(echo "$CENTRAL_ALERTS" | jq -r \
'.data.items[] | select(.alertType | ascii_downcase | contains("critical") and contains("potassium")) | select(.syncedFromGateway == true) | .acknowledgedBy // empty' \
| head -1)
if [[ "$ACK_BY" == "RN-Wu" ]]; then
log "Central acknowledgedBy = RN-Wu"
else
log "Checking central DB for acknowledged_by (API returned: ${ACK_BY:-<empty>})"
docker exec "$(docker ps -qf name=postgres)" psql -U postgres -d vigilcare -tAc \
"SELECT acknowledged_by FROM clinical_alerts WHERE encounter_id = '${ENCOUNTER_ID}' AND alert_type = 'CRITICAL_POTASSIUM_MEQ_L' LIMIT 1;" \
| grep -q "RN-Wu" \
|| die "Central acknowledged_by is not RN-Wu after sync"
log "Central DB acknowledged_by = RN-Wu"
fi
log "Phase 24 verification passed."
log "Logs: $LOG_DIR"