chore: update readme
This commit is contained in:
@@ -2,8 +2,7 @@
|
||||
# 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, 404 OperationOutcome,
|
||||
# and FhirIntegrationTests.
|
||||
# LOINC mapping, $everything, content-type negotiation, and 404 OperationOutcome.
|
||||
#
|
||||
# Prerequisites:
|
||||
# docker compose up -d (PostgreSQL, Redis, MinIO)
|
||||
@@ -20,7 +19,6 @@
|
||||
# VIGILCARE_PG_USER default: postgres
|
||||
# VIGILCARE_PG_PASSWORD default: password
|
||||
# VIGILCARE_SKIP_DB_CHECKS set to 1 to skip PostgreSQL seed/assertions
|
||||
# VIGILCARE_SKIP_TEST_CHECKS set to 1 to skip dotnet integration tests
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x scripts/run-vigilcare-records-phase-11-verification.sh
|
||||
@@ -41,7 +39,6 @@ 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}"
|
||||
SKIP_TEST_CHECKS="${VIGILCARE_SKIP_TEST_CHECKS:-0}"
|
||||
|
||||
# Deterministic clinical IDs (match FhirClinicalSeedHelper)
|
||||
PATIENT1_ID="b1000000-0000-0000-0000-000000000001"
|
||||
@@ -53,6 +50,8 @@ 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
|
||||
@@ -196,6 +195,106 @@ test_authentication() {
|
||||
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"
|
||||
|
||||
@@ -206,70 +305,21 @@ ensure_fhir_clinical_seed() {
|
||||
return
|
||||
fi
|
||||
|
||||
local patient_count
|
||||
patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")"
|
||||
|
||||
if [[ "${patient_count:-0}" -ge 1 ]]; then
|
||||
pass "clinical patient VCR-000001 already present"
|
||||
if verification_fixture_ready; then
|
||||
pass "FHIR verification fixture (VCR-000001 + HEART_RATE) already present"
|
||||
return
|
||||
fi
|
||||
|
||||
log " Seeding demo clinical records for FHIR verification..."
|
||||
log " Ensuring FHIR verification fixture for VCR-000001..."
|
||||
|
||||
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;
|
||||
" || {
|
||||
fail "seed clinical.patients for FHIR verification"
|
||||
return
|
||||
}
|
||||
|
||||
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}', '${PATIENT1_ID}', NOW() - interval '5 days',
|
||||
'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC',
|
||||
'active', '${BATCH1_ID}', NOW() - interval '3 days', NOW() - interval '12 hours'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
" || {
|
||||
fail "seed clinical.encounters for FHIR verification"
|
||||
return
|
||||
}
|
||||
|
||||
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}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}',
|
||||
'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days',
|
||||
'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours'),
|
||||
('${WBC_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}',
|
||||
'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days',
|
||||
'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
" || {
|
||||
fail "seed clinical.observations for FHIR verification"
|
||||
return
|
||||
}
|
||||
|
||||
patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")"
|
||||
if [[ "${patient_count:-0}" -ge 1 ]]; then
|
||||
pass "seeded clinical patient VCR-000001 for FHIR verification"
|
||||
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 "seeded clinical patient VCR-000001 for FHIR verification"
|
||||
fail "seed FHIR verification fixture for VCR-000001"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -293,22 +343,68 @@ test_fhir_metadata() {
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_patient_id() {
|
||||
section "3. Resolve FHIR Patient ID"
|
||||
resolve_clinical_ids() {
|
||||
section "3. Resolve FHIR clinical resource IDs"
|
||||
|
||||
local search_json
|
||||
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)"
|
||||
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
|
||||
RESOLVED_PATIENT_ID="$PATIENT1_ID"
|
||||
log " NOTE: Patient search returned no entries; using seeded ID $PATIENT1_ID"
|
||||
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 FHIR patient ID ($RESOLVED_PATIENT_ID)"
|
||||
pass "resolved patient ID ($RESOLVED_PATIENT_ID)"
|
||||
else
|
||||
fail "resolved FHIR patient ID"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -351,7 +447,7 @@ test_fhir_encounter() {
|
||||
section "5. FHIR Encounter read & search"
|
||||
|
||||
local encounter_json status subject patient_search
|
||||
encounter_json="$(fhir_get "/Encounter/${ENCOUNTER1_ID}" "$ADMIN_TOKEN")"
|
||||
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"
|
||||
@@ -378,8 +474,8 @@ test_fhir_encounter() {
|
||||
test_fhir_observation() {
|
||||
section "6. FHIR Observation read, LOINC search, category & date"
|
||||
|
||||
local obs_json loinc_code loinc_unit loinc_value loinc_search category_search date_search
|
||||
obs_json="$(fhir_get "/Observation/${HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")"
|
||||
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"
|
||||
@@ -389,8 +485,8 @@ test_fhir_observation() {
|
||||
|
||||
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)"
|
||||
loinc_value="$(jq -er '.valueQuantity.value // empty' <<<"$obs_json" 2>/dev/null || true)"
|
||||
if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" && "$loinc_value" == "88" ]]; then
|
||||
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"
|
||||
@@ -410,11 +506,18 @@ test_fhir_observation() {
|
||||
fail "GET /fhir/Observation?category=vital-signs returns vital sign observations"
|
||||
fi
|
||||
|
||||
date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge2026-06-20" "$ADMIN_TOKEN")"
|
||||
if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
|
||||
pass "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt"
|
||||
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
|
||||
fail "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -510,31 +613,8 @@ test_bundle_pagination() {
|
||||
fi
|
||||
}
|
||||
|
||||
test_integration_tests() {
|
||||
section "11. dotnet integration tests — FhirIntegrationTests"
|
||||
|
||||
if [[ "$SKIP_TEST_CHECKS" == "1" ]]; then
|
||||
log " SKIP: dotnet integration tests (VIGILCARE_SKIP_TEST_CHECKS=1)"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v dotnet >/dev/null 2>&1; then
|
||||
log " SKIP: dotnet not installed"
|
||||
return
|
||||
fi
|
||||
|
||||
if dotnet test "$REPO_ROOT/VigilCareRecordsAPI.Tests/VigilCareRecordsAPI.Tests.csproj" \
|
||||
--filter "FullyQualifiedName~FhirIntegrationTests" \
|
||||
--no-restore >/tmp/vigilcare-p11-tests.log 2>&1; then
|
||||
pass "FhirIntegrationTests passed"
|
||||
else
|
||||
fail "FhirIntegrationTests passed"
|
||||
log " see /tmp/vigilcare-p11-tests.log"
|
||||
fi
|
||||
}
|
||||
|
||||
print_manual_ui_checklist() {
|
||||
section "12. Manual Vue UI checks (plan §7)"
|
||||
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"
|
||||
@@ -557,7 +637,7 @@ main() {
|
||||
test_authentication
|
||||
ensure_fhir_clinical_seed
|
||||
test_fhir_metadata
|
||||
resolve_patient_id
|
||||
resolve_clinical_ids
|
||||
test_fhir_patient_read
|
||||
test_fhir_encounter
|
||||
test_fhir_observation
|
||||
@@ -565,7 +645,6 @@ main() {
|
||||
test_fhir_content_type
|
||||
test_fhir_error_handling
|
||||
test_bundle_pagination
|
||||
test_integration_tests
|
||||
print_manual_ui_checklist
|
||||
|
||||
log ""
|
||||
|
||||
Reference in New Issue
Block a user