#!/usr/bin/env bash # Runs Phase 11 verification checks from docs/plans/phase-11-plan.md. # # Covers FHIR metadata, Patient/Encounter/Observation read & search, # LOINC mapping, $everything, content-type negotiation, and 404 OperationOutcome. # # Prerequisites: # docker compose up -d (PostgreSQL, Redis, MinIO) # dotnet ef database update --project VigilCareRecordsAPI # dotnet run --project VigilCareRecordsAPI # Phase 1–10 seed data (admin1) # # Environment overrides: # VIGILCARE_API_URL default: http://localhost:5217 # VIGILCARE_COMPOSE_FILE default: /docker-compose.yml # VIGILCARE_PG_HOST default: localhost # VIGILCARE_PG_PORT default: 5437 # VIGILCARE_PG_DB default: vigilcare_records # VIGILCARE_PG_USER default: postgres # VIGILCARE_PG_PASSWORD default: password # VIGILCARE_SKIP_DB_CHECKS set to 1 to skip PostgreSQL seed/assertions # # Usage: # chmod +x scripts/run-vigilcare-records-phase-11-verification.sh # ./scripts/run-vigilcare-records-phase-11-verification.sh set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" API_URL="${VIGILCARE_API_URL:-http://localhost:5217}" FHIR_URL="${API_URL}/fhir" COMPOSE_FILE="${VIGILCARE_COMPOSE_FILE:-$REPO_ROOT/docker-compose.yml}" COMPOSE=(docker compose -f "$COMPOSE_FILE") PG_HOST="${VIGILCARE_PG_HOST:-localhost}" PG_PORT="${VIGILCARE_PG_PORT:-5437}" PG_DB="${VIGILCARE_PG_DB:-vigilcare_records}" PG_USER="${VIGILCARE_PG_USER:-postgres}" PG_PASSWORD="${VIGILCARE_PG_PASSWORD:-password}" SKIP_DB_CHECKS="${VIGILCARE_SKIP_DB_CHECKS:-0}" # Deterministic clinical IDs (match FhirClinicalSeedHelper) PATIENT1_ID="b1000000-0000-0000-0000-000000000001" PATIENT2_ID="b1000000-0000-0000-0000-000000000002" ENCOUNTER1_ID="d1000000-0000-0000-0000-000000000001" HEART_RATE_OBS_ID="e1000000-0000-0000-0000-000000000001" WBC_OBS_ID="e1000000-0000-0000-0000-000000000002" BATCH1_ID="c1000000-0000-0000-0000-000000000001" ADMIN_TOKEN="" RESOLVED_PATIENT_ID="" RESOLVED_ENCOUNTER_ID="" RESOLVED_HEART_RATE_OBS_ID="" PASS_COUNT=0 FAIL_COUNT=0 FAILED_TESTS=() log() { printf '%s\n' "$*" } section() { log "" log "== $1 ==" } pass() { PASS_COUNT=$((PASS_COUNT + 1)) log " PASS: $1" } fail() { FAIL_COUNT=$((FAIL_COUNT + 1)) FAILED_TESTS+=("$1") log " FAIL: $1" } require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then log "ERROR: required command not found: $cmd" exit 1 fi } http_code() { curl -sS -o /dev/null -w '%{http_code}' "$@" } json_post() { local url="$1" local body="$2" curl -sS -X POST "$url" \ -H 'Content-Type: application/json' \ -d "$body" } fhir_get() { local path_query="$1" local token="${2:-}" if [[ -n "$token" ]]; then curl -sS "${FHIR_URL}${path_query}" \ -H "Authorization: Bearer $token" \ -H 'Accept: application/fhir+json' else curl -sS "${FHIR_URL}${path_query}" \ -H 'Accept: application/fhir+json' fi } fhir_get_status() { local path_query="$1" local token="$2" local body_file http_status body_file="$(mktemp)" http_status="$(curl -sS -o "$body_file" -w '%{http_code}' \ "${FHIR_URL}${path_query}" \ -H "Authorization: Bearer $token" \ -H 'Accept: application/fhir+json')" cat "$body_file" rm -f "$body_file" printf '\n__HTTP_STATUS__:%s' "$http_status" } assert_api_reachable() { local code code="$(http_code "$API_URL/swagger/index.html" || true)" if [[ "$code" != "200" ]]; then log "ERROR: API not reachable at $API_URL (HTTP $code)." log "Start infrastructure with: docker compose up -d" log "Apply migrations with: dotnet ef database update --project VigilCareRecordsAPI" log "Start API with: dotnet run --project VigilCareRecordsAPI" exit 1 fi } compose_service_running() { local service="$1" "${COMPOSE[@]}" ps --status running --services 2>/dev/null | grep -qx "$service" } psql_available() { [[ "$SKIP_DB_CHECKS" == "1" ]] && return 1 compose_service_running postgres && return 0 command -v psql >/dev/null 2>&1 && return 0 return 1 } psql_query() { if [[ "$SKIP_DB_CHECKS" == "1" ]]; then return 1 fi if compose_service_running postgres; then "${COMPOSE[@]}" exec -T postgres \ psql -U "$PG_USER" -d "$PG_DB" -Atqc "$1" elif command -v psql >/dev/null 2>&1; then PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -Atqc "$1" else return 1 fi } psql_exec() { if [[ "$SKIP_DB_CHECKS" == "1" ]]; then return 1 fi if compose_service_running postgres; then "${COMPOSE[@]}" exec -T postgres \ psql -U "$PG_USER" -d "$PG_DB" -v ON_ERROR_STOP=1 -q -c "$1" elif command -v psql >/dev/null 2>&1; then PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \ -v ON_ERROR_STOP=1 -q -c "$1" else return 1 fi } test_authentication() { section "0. Authentication" local admin_json admin_json="$(json_post "$API_URL/api/v1/auth/login" \ '{"username":"admin1","password":"password"}')" ADMIN_TOKEN="$(jq -er '.data.token // empty' <<<"$admin_json" 2>/dev/null || true)" if [[ -n "$ADMIN_TOKEN" ]]; then pass "admin1 login returns JWT" else log "ERROR: admin1 login failed." log "Response: ${admin_json:-}" exit 1 fi } verification_fixture_ready() { local heart_rate_count heart_rate_count="$(psql_query " SELECT count(*) FROM clinical.observations o JOIN clinical.patients p ON p.id = o.patient_id WHERE p.mrn = 'VCR-000001' AND o.observation_code = 'HEART_RATE'; " 2>/dev/null || echo "0")" [[ "${heart_rate_count:-0}" -ge 1 ]] } seed_verification_fixture() { local patient_id encounter_id batch_id heart_rate_count patient_id="$(psql_query "SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1;" 2>/dev/null || true)" if [[ -z "$patient_id" ]]; then psql_exec " INSERT INTO clinical.patients ( id, mrn, full_name, date_of_birth, sex, blood_type, emergency_contact, allergies_json, no_known_allergies, created_at, updated_at ) VALUES ('${PATIENT1_ID}', 'VCR-000001', 'MARIA SANTOS', '1978-03-15', 'female', 'A+', 'Juan Santos - 555-0101', '[\"Penicillin\", \"Sulfa drugs\"]', false, NOW() - interval '3 days', NOW() - interval '12 hours'), ('${PATIENT2_ID}', 'VCR-000002', 'KENJI NAKAMURA', '1952-11-08', 'male', 'O-', 'Yuki Nakamura - 555-0202', NULL, true, NOW() - interval '1 day', NOW() - interval '1 day') ON CONFLICT (id) DO NOTHING; " || return 1 patient_id="$PATIENT1_ID" else psql_exec " UPDATE clinical.patients SET full_name = 'MARIA SANTOS', date_of_birth = '1978-03-15', sex = 'female', blood_type = 'A+', emergency_contact = 'Juan Santos - 555-0101', allergies_json = '[\"Penicillin\", \"Sulfa drugs\"]', no_known_allergies = false, updated_at = NOW() WHERE id = '${patient_id}'; " || return 1 fi encounter_id="$(psql_query " SELECT id FROM clinical.encounters WHERE patient_id = '${patient_id}' ORDER BY created_at LIMIT 1; " 2>/dev/null || true)" if [[ -z "$encounter_id" ]]; then batch_id="$(psql_query "SELECT id FROM digitization_batches ORDER BY created_at LIMIT 1;" 2>/dev/null || true)" batch_id="${batch_id:-$BATCH1_ID}" psql_exec " INSERT INTO clinical.encounters ( id, patient_id, admission_date, department, room_bed, admission_reason, status, source_batch_id, created_at, updated_at ) VALUES ( '${ENCOUNTER1_ID}', '${patient_id}', NOW() - interval '5 days', 'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC', 'active', '${batch_id}', NOW() - interval '3 days', NOW() - interval '12 hours' ) ON CONFLICT (id) DO NOTHING; " || return 1 encounter_id="$ENCOUNTER1_ID" fi batch_id="$(psql_query " SELECT source_batch_id FROM clinical.encounters WHERE id = '${encounter_id}' LIMIT 1; " 2>/dev/null || true)" batch_id="${batch_id:-$BATCH1_ID}" heart_rate_count="$(psql_query " SELECT count(*) FROM clinical.observations WHERE patient_id = '${patient_id}' AND observation_code = 'HEART_RATE'; " 2>/dev/null || echo "0")" if [[ "${heart_rate_count:-0}" -lt 1 ]]; then psql_exec " INSERT INTO clinical.observations ( id, encounter_id, patient_id, observation_code, value, unit, recorded_at, source, source_batch_id, created_at ) VALUES ('${HEART_RATE_OBS_ID}', '${encounter_id}', '${patient_id}', 'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days', 'digitization_backfill', '${batch_id}', NOW() - interval '12 hours'), ('${WBC_OBS_ID}', '${encounter_id}', '${patient_id}', 'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days', 'digitization_backfill', '${batch_id}', NOW() - interval '12 hours') ON CONFLICT (id) DO NOTHING; " || return 1 fi return 0 } ensure_fhir_clinical_seed() { section "1. Clinical seed data for FHIR endpoints" if ! psql_available; then log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)" log " NOTE: FHIR curl tests require clinical.patients with MRN VCR-000001" log " Promote a batch or run with postgres available to auto-seed." return fi if verification_fixture_ready; then pass "FHIR verification fixture (VCR-000001 + HEART_RATE) already present" return fi log " Ensuring FHIR verification fixture for VCR-000001..." if seed_verification_fixture; then if verification_fixture_ready; then pass "FHIR verification fixture ready for VCR-000001" else fail "FHIR verification fixture ready for VCR-000001" fi else fail "seed FHIR verification fixture for VCR-000001" fi } test_fhir_metadata() { section "2. FHIR metadata — GET /fhir/metadata" local metadata types metadata="$(fhir_get '/metadata')" types="$(jq -r '.rest[0].resource[].type' <<<"$metadata" 2>/dev/null | sort | tr '\n' ' ')" if jq -e '.resourceType == "CapabilityStatement"' <<<"$metadata" >/dev/null 2>&1; then pass "metadata returns CapabilityStatement" else fail "metadata returns CapabilityStatement" fi if grep -q 'Patient' <<<"$types" && grep -q 'Encounter' <<<"$types" && grep -q 'Observation' <<<"$types"; then pass "metadata lists Patient, Encounter, Observation resources" else fail "metadata lists Patient, Encounter, Observation resources (got: $types)" fi } resolve_clinical_ids() { section "3. Resolve FHIR clinical resource IDs" local search_json encounter_search obs_search if psql_available; then RESOLVED_PATIENT_ID="$(psql_query " SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1; " 2>/dev/null || true)" if [[ -n "$RESOLVED_PATIENT_ID" ]]; then RESOLVED_ENCOUNTER_ID="$(psql_query " SELECT id FROM clinical.encounters WHERE patient_id = '${RESOLVED_PATIENT_ID}' ORDER BY created_at LIMIT 1; " 2>/dev/null || true)" RESOLVED_HEART_RATE_OBS_ID="$(psql_query " SELECT id FROM clinical.observations WHERE patient_id = '${RESOLVED_PATIENT_ID}' AND observation_code = 'HEART_RATE' ORDER BY recorded_at DESC LIMIT 1; " 2>/dev/null || true)" fi fi if [[ -z "$RESOLVED_PATIENT_ID" ]]; then search_json="$(fhir_get "/Patient?identifier=VCR-000001" "$ADMIN_TOKEN")" RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)" fi if [[ -z "$RESOLVED_PATIENT_ID" ]]; then search_json="$(fhir_get "/Patient?name=Santos" "$ADMIN_TOKEN")" RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)" fi if [[ -z "$RESOLVED_ENCOUNTER_ID" && -n "$RESOLVED_PATIENT_ID" ]]; then encounter_search="$(fhir_get "/Encounter?patient=${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")" RESOLVED_ENCOUNTER_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$encounter_search" 2>/dev/null || true)" fi if [[ -z "$RESOLVED_HEART_RATE_OBS_ID" && -n "$RESOLVED_PATIENT_ID" ]]; then obs_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&code=8867-4" "$ADMIN_TOKEN")" RESOLVED_HEART_RATE_OBS_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$obs_search" 2>/dev/null || true)" fi if [[ -n "$RESOLVED_PATIENT_ID" ]]; then pass "resolved patient ID ($RESOLVED_PATIENT_ID)" else fail "resolved patient ID" fi if [[ -n "$RESOLVED_ENCOUNTER_ID" ]]; then pass "resolved encounter ID ($RESOLVED_ENCOUNTER_ID)" else fail "resolved encounter ID" fi if [[ -n "$RESOLVED_HEART_RATE_OBS_ID" ]]; then pass "resolved HEART_RATE observation ID ($RESOLVED_HEART_RATE_OBS_ID)" else fail "resolved HEART_RATE observation ID" fi } test_fhir_patient_read() { section "4. FHIR Patient read & search" local patient_json mrn gender birth_date identifier_json patient_json="$(fhir_get "/Patient/${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")" if jq -e '.resourceType == "Patient"' <<<"$patient_json" >/dev/null 2>&1; then pass "GET /fhir/Patient/{id} returns Patient resource" else fail "GET /fhir/Patient/{id} returns Patient resource" fi mrn="$(jq -er '.identifier[0].value // empty' <<<"$patient_json" 2>/dev/null || true)" if [[ "$mrn" == "VCR-000001" ]]; then pass "Patient read includes MRN identifier VCR-000001" else fail "Patient read includes MRN identifier VCR-000001 (got: ${mrn:-})" fi gender="$(jq -er '.gender // empty' <<<"$patient_json" 2>/dev/null || true)" birth_date="$(jq -er '.birthDate // empty' <<<"$patient_json" 2>/dev/null || true)" if [[ "$gender" == "female" && "$birth_date" == "1978-03-15" ]]; then pass "Patient read includes gender and birthDate" else fail "Patient read includes gender and birthDate (gender=$gender birthDate=$birth_date)" fi identifier_json="$(fhir_get "/Patient?identifier=VCR-000001" "$ADMIN_TOKEN")" if [[ "$(jq -er '.total // 0' <<<"$identifier_json")" -ge 1 ]]; then pass "GET /fhir/Patient?identifier=VCR-000001 returns matches" else fail "GET /fhir/Patient?identifier=VCR-000001 returns matches" fi } test_fhir_encounter() { section "5. FHIR Encounter read & search" local encounter_json status subject patient_search encounter_json="$(fhir_get "/Encounter/${RESOLVED_ENCOUNTER_ID}" "$ADMIN_TOKEN")" if jq -e '.resourceType == "Encounter"' <<<"$encounter_json" >/dev/null 2>&1; then pass "GET /fhir/Encounter/{id} returns Encounter resource" else fail "GET /fhir/Encounter/{id} returns Encounter resource" fi status="$(jq -er '.status // empty' <<<"$encounter_json" 2>/dev/null || true)" subject="$(jq -er '.subject.reference // empty' <<<"$encounter_json" 2>/dev/null || true)" if [[ "$status" == "in-progress" && "$subject" == "Patient/${RESOLVED_PATIENT_ID}" ]]; then pass "Encounter read has in-progress status and patient reference" else fail "Encounter read has in-progress status and patient reference" fi patient_search="$(fhir_get "/Encounter?patient=${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")" if [[ "$(jq -er '.total // 0' <<<"$patient_search")" -ge 1 ]]; then pass "GET /fhir/Encounter?patient={id} returns matches" else fail "GET /fhir/Encounter?patient={id} returns matches" fi } test_fhir_observation() { section "6. FHIR Observation read, LOINC search, category & date" local obs_json loinc_code loinc_unit loinc_search category_search date_search date_filter obs_json="$(fhir_get "/Observation/${RESOLVED_HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")" if jq -e '.resourceType == "Observation"' <<<"$obs_json" >/dev/null 2>&1; then pass "GET /fhir/Observation/{id} returns Observation resource" else fail "GET /fhir/Observation/{id} returns Observation resource" fi loinc_code="$(jq -er '.code.coding[0].code // empty' <<<"$obs_json" 2>/dev/null || true)" loinc_unit="$(jq -er '.valueQuantity.unit // empty' <<<"$obs_json" 2>/dev/null || true)" if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" ]] \ && jq -e '.valueQuantity.value == 88' <<<"$obs_json" >/dev/null 2>&1; then pass "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm" else fail "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm" fi loinc_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&code=8867-4" "$ADMIN_TOKEN")" if [[ "$(jq -er '.entry | length' <<<"$loinc_search" 2>/dev/null || echo 0)" -ge 1 ]]; then pass "GET /fhir/Observation?code=8867-4 resolves LOINC to HEART_RATE" else fail "GET /fhir/Observation?code=8867-4 resolves LOINC to HEART_RATE" fi category_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&category=vital-signs" "$ADMIN_TOKEN")" if [[ "$(jq -er '.entry | length' <<<"$category_search" 2>/dev/null || echo 0)" -ge 1 ]]; then pass "GET /fhir/Observation?category=vital-signs returns vital sign observations" else fail "GET /fhir/Observation?category=vital-signs returns vital sign observations" fi date_filter="$(jq -er '.effectiveDateTime // empty' <<<"$obs_json" 2>/dev/null | cut -c1-10 || true)" if [[ -n "$date_filter" ]]; then date_filter="$(date -d "${date_filter} - 1 day" +%Y-%m-%d 2>/dev/null || echo "2026-06-20")" else date_filter="2026-06-20" fi date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge${date_filter}" "$ADMIN_TOKEN")" if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then pass "GET /fhir/Observation?date=ge${date_filter} filters by recordedAt" else fail "GET /fhir/Observation?date=ge${date_filter} filters by recordedAt" fi } test_fhir_patient_everything() { section "7. FHIR Patient \$everything" local bundle_json total resource_types has_patient has_encounter has_observation bundle_json="$(fhir_get "/Patient/${RESOLVED_PATIENT_ID}/\$everything" "$ADMIN_TOKEN")" total="$(jq -er '.total // 0' <<<"$bundle_json" 2>/dev/null || echo 0)" resource_types="$(jq -r '[.entry[]?.resource.resourceType] | join(",")' <<<"$bundle_json" 2>/dev/null || true)" if [[ "$total" -gt 0 ]]; then pass "GET /fhir/Patient/{id}/\$everything returns non-empty Bundle" else fail "GET /fhir/Patient/{id}/\$everything returns non-empty Bundle" fi has_patient="$(grep -c 'Patient' <<<"$resource_types" || true)" has_encounter="$(grep -c 'Encounter' <<<"$resource_types" || true)" has_observation="$(grep -c 'Observation' <<<"$resource_types" || true)" if [[ "$has_patient" -ge 1 && "$has_encounter" -ge 1 && "$has_observation" -ge 1 ]]; then pass "\$everything Bundle contains Patient, Encounter, and Observation" else fail "\$everything Bundle contains Patient, Encounter, and Observation (types: $resource_types)" fi } test_fhir_content_type() { section "8. Content-Type negotiation" local content_type content_type="$(curl -sS -o /dev/null -w '%{content_type}' \ "${FHIR_URL}/Patient/${RESOLVED_PATIENT_ID}" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H 'Accept: application/fhir+json')" if [[ "$content_type" == application/fhir+json* ]]; then pass "Patient read Content-Type is application/fhir+json" else fail "Patient read Content-Type is application/fhir+json (got: $content_type)" fi } test_fhir_error_handling() { section "9. FHIR error handling — 404 OperationOutcome" local response http_status issue_code issue_severity response="$(fhir_get_status "/Patient/00000000-0000-0000-0000-000000000000" "$ADMIN_TOKEN")" http_status="${response##*__HTTP_STATUS__:}" response="${response%__HTTP_STATUS__:*}" issue_code="$(jq -er '.issue[0].code // empty' <<<"$response" 2>/dev/null || true)" issue_severity="$(jq -er '.issue[0].severity // empty' <<<"$response" 2>/dev/null || true)" if [[ "$http_status" == "404" ]]; then pass "unknown Patient returns HTTP 404" else fail "unknown Patient returns HTTP 404 (got HTTP $http_status)" fi if [[ "$issue_code" == "not-found" && "$issue_severity" == "error" ]]; then pass "404 response is OperationOutcome with not-found issue" else fail "404 response is OperationOutcome with not-found issue" fi } test_bundle_pagination() { section "10. Bundle pagination links" local bundle_json bundle_json="$(fhir_get "/Patient?_count=1&_offset=0" "$ADMIN_TOKEN")" if [[ "$(jq -er '.total // 0' <<<"$bundle_json")" -ge 2 ]]; then pass "Patient search total >= 2 for pagination test" else fail "Patient search total >= 2 for pagination test" return fi if jq -e '.link[] | select(.relation == "self")' <<<"$bundle_json" >/dev/null 2>&1; then pass "search Bundle includes self link" else fail "search Bundle includes self link" fi if jq -e '.link[] | select(.relation == "next")' <<<"$bundle_json" >/dev/null 2>&1; then pass "search Bundle includes next link" else fail "search Bundle includes next link" fi } print_manual_ui_checklist() { section "11. Manual Vue UI checks (plan §7)" log " Login as admin1 → http://localhost:3028/fhir-explorer" log " - Select Patient resource type; search by name Santos" log " - Results table shows matching patients" log " - Click a row to see full FHIR JSON" log " - Patient \$everything: select patient, Load All Data" log " - Grouped Patient summary, Encounters list, Observations timeline" log " - Open CapabilityStatement link opens metadata JSON in new tab" } main() { require_cmd curl require_cmd jq log "VigilCare Records — Phase 11 verification" log "API: $API_URL" log "FHIR: $FHIR_URL" assert_api_reachable test_authentication ensure_fhir_clinical_seed test_fhir_metadata resolve_clinical_ids test_fhir_patient_read test_fhir_encounter test_fhir_observation test_fhir_patient_everything test_fhir_content_type test_fhir_error_handling test_bundle_pagination print_manual_ui_checklist log "" log "Results: $PASS_COUNT passed, $FAIL_COUNT failed" if (( FAIL_COUNT > 0 )); then log "Failed checks:" for item in "${FAILED_TESTS[@]}"; do log " - $item" done exit 1 fi log "All Phase 11 API verification checks passed." log "Complete the manual Vue FHIR Explorer checklist above if not already done." } main "$@"