#!/usr/bin/env bash # Runs Phase 10 verification checks from docs/plans/phase-10-plan.md. # # Covers cover sheet generation, lookup, list filters, PDF endpoints, # barcode-assisted batch upload, redeem/auto-assign, and reuse prevention. # # Prerequisites: # docker compose up -d (PostgreSQL, Redis, MinIO) # dotnet ef database update --project VigilCareRecordsAPI # dotnet run --project VigilCareRecordsAPI # Phase 1–9 seed data (intake1, entry1, 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 assertions # VIGILCARE_SKIP_TEST_CHECKS set to 1 to skip dotnet integration tests # # Usage: # chmod +x scripts/run-vigilcare-records-phase-10-verification.sh # ./scripts/run-vigilcare-records-phase-10-verification.sh set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" FIXTURE_PDF="$SCRIPT_DIR/fixtures/test-scan.pdf" API_URL="${VIGILCARE_API_URL:-http://localhost:5217}" 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}" SKIP_TEST_CHECKS="${VIGILCARE_SKIP_TEST_CHECKS:-0}" # Resolved at runtime from the live API (seed IDs vary per environment) ENTRY_CLERK1_ID="" PATIENT1_ID="" PASS_COUNT=0 FAIL_COUNT=0 FAILED_TESTS=() # Shared state populated during the run INTAKE_TOKEN="" ADMIN_TOKEN="" ENTRY_TOKEN="" UPLOAD_CODE="" UPLOAD_COVER_SHEET_ID="" UPLOAD_BATCH_ID="" ASSIGN_CODE="" PDF_SHEET_IDS=() 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 } ensure_fixture_pdf() { if [[ -f "$FIXTURE_PDF" ]]; then return 0 fi mkdir -p "$(dirname "$FIXTURE_PDF")" cat >"$FIXTURE_PDF" <<'EOF' %PDF-1.0 1 0 obj<>endobj 2 0 obj<>endobj 3 0 obj<>>>endobj xref 0 4 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n trailer<> startxref 206 %%EOF EOF } unique_pdf_path() { local suffix="$1" local path="/tmp/vigilcare-p10-${suffix}-${RANDOM}.pdf" printf '%%PDF-1.4\nphase10-%s-%s\n%%%%EOF\n' "$suffix" "$(date +%s%N)" >"$path" printf '%s' "$path" } http_code() { curl -sS -o /dev/null -w '%{http_code}' "$@" } json_post() { local url="$1" local body="$2" local token="${3:-}" if [[ -n "$token" ]]; then curl -sS -X POST "$url" \ -H "Authorization: Bearer $token" \ -H 'Content-Type: application/json' \ -d "$body" else curl -sS -X POST "$url" \ -H 'Content-Type: application/json' \ -d "$body" fi } json_get() { local url="$1" local token="$2" curl -sS "$url" -H "Authorization: Bearer $token" } login() { local username="$1" local password="${2:-password}" json_post "$API_URL/api/v1/auth/login" \ "{\"username\":\"$username\",\"password\":\"$password\"}" } extract_data_field() { local json="$1" local field="$2" jq -er ".data.$field // empty" <<<"$json" } extract_bool_field() { local json="$1" local field="$2" jq -er ".data.$field | if . == null then empty else tostring end" <<<"$json" } resolve_directory_ids() { local users_json patients_json users_json="$(json_get "$API_URL/api/v1/users?role=DATA_ENTRY_CLERK" "$INTAKE_TOKEN")" patients_json="$(json_get "$API_URL/api/v1/patients/search?q=Patient" "$INTAKE_TOKEN")" ENTRY_CLERK1_ID="$(jq -er '.data[0].id // empty' <<<"$users_json" 2>/dev/null || true)" PATIENT1_ID="$(jq -er '.data[0].id // empty' <<<"$patients_json" 2>/dev/null || true)" if [[ -n "$ENTRY_CLERK1_ID" ]]; then pass "resolved entry clerk ID for auto-assign test" else fail "resolved entry clerk ID for auto-assign test" fi if [[ -n "$PATIENT1_ID" ]]; then pass "resolved patient ID for patient-linked cover sheet test" else fail "resolved patient ID for patient-linked cover sheet test" fi } extract_error_code() { local json="$1" jq -er '.error.code // empty' <<<"$json" 2>/dev/null || true } 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 } upload_with_cover_sheet() { local token="$1" local code="$2" local pdf="$3" curl -sS -X POST "$API_URL/api/v1/digitization-batches" \ -H "Authorization: Bearer $token" \ -F "file=@${pdf};type=application/pdf" \ -F "coverSheetCode=$code" } upload_with_cover_sheet_status() { local token="$1" local code="$2" local pdf="$3" local body_file http_code_val body_file="$(mktemp)" http_code_val="$(curl -sS -o "$body_file" -w '%{http_code}' -X POST "$API_URL/api/v1/digitization-batches" \ -H "Authorization: Bearer $token" \ -F "file=@${pdf};type=application/pdf" \ -F "coverSheetCode=$code")" cat "$body_file" rm -f "$body_file" printf '\n__HTTP_STATUS__:%s' "$http_code_val" } generate_cover_sheets() { local token="$1" local body="$2" json_post "$API_URL/api/v1/cover-sheets/generate" "$body" "$token" } test_authentication() { section "0. Authentication" local intake_json admin_json entry_json intake_json="$(login intake1)" admin_json="$(login admin1)" entry_json="$(login entry1)" INTAKE_TOKEN="$(extract_data_field "$intake_json" token)" ADMIN_TOKEN="$(extract_data_field "$admin_json" token)" ENTRY_TOKEN="$(extract_data_field "$entry_json" token)" if [[ -z "$INTAKE_TOKEN" ]]; then log "ERROR: intake1 login failed." log "Response: ${intake_json:-}" exit 1 fi if [[ -n "$INTAKE_TOKEN" ]]; then pass "intake1 login returns JWT" else fail "intake1 login returns JWT" fi if [[ -n "$ADMIN_TOKEN" ]]; then pass "admin1 login returns JWT" else fail "admin1 login returns JWT" fi if [[ -n "$ENTRY_TOKEN" ]]; then pass "entry1 login returns JWT" else fail "entry1 login returns JWT" fi resolve_directory_ids } test_database_schema() { section "1. Database — cover_sheets table" if ! psql_available; then log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)" return fi local table_exists index_code index_unused table_exists="$(psql_query " SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'cover_sheets'; ")" index_code="$(psql_query " SELECT count(*) FROM pg_indexes WHERE tablename = 'cover_sheets' AND indexname = 'ix_cover_sheets_code'; ")" index_unused="$(psql_query " SELECT count(*) FROM pg_indexes WHERE tablename = 'cover_sheets' AND indexname = 'ix_cover_sheets_unused'; ")" if [[ "$table_exists" == "1" ]]; then pass "cover_sheets table exists" else fail "cover_sheets table exists" fi if [[ "$index_code" == "1" ]]; then pass "unique index ix_cover_sheets_code exists" else fail "unique index ix_cover_sheets_code exists" fi if [[ "$index_unused" == "1" ]]; then pass "filtered index ix_cover_sheets_unused exists" else fail "filtered index ix_cover_sheets_unused exists" fi } test_cover_sheet_generation() { section "2. Cover sheet generation (plan §1)" local gen_json count unique_count bad_format gen_json="$(generate_cover_sheets "$INTAKE_TOKEN" \ '{"count":5,"batchType":"VITALS_SHEET","track":"BACKFILL"}')" if [[ "$(jq -er '.success' <<<"$gen_json")" == "true" ]]; then pass "POST /cover-sheets/generate returns success for intake1" else fail "POST /cover-sheets/generate returns success for intake1" log " response: $gen_json" return fi count="$(jq '.data | length' <<<"$gen_json")" unique_count="$(jq '[.data[].code] | unique | length' <<<"$gen_json")" if [[ "$count" == "5" && "$unique_count" == "5" ]]; then pass "generate creates 5 cover sheets with unique codes" else fail "generate creates 5 cover sheets with unique codes (count=$count unique=$unique_count)" fi bad_format="$(jq -r '[.data[].code | test("^VCR-CS-[0-9A-F]{8}$")] | all' <<<"$gen_json")" if [[ "$bad_format" == "true" ]]; then pass "all codes match VCR-CS-{8-hex} format" else fail "all codes match VCR-CS-{8-hex} format" fi UPLOAD_CODE="$(jq -r '.data[0].code' <<<"$gen_json")" UPLOAD_COVER_SHEET_ID="$(jq -r '.data[0].id' <<<"$gen_json")" PDF_SHEET_IDS=($(jq -r '.data[0].id, .data[1].id' <<<"$gen_json")) local entry_json entry_token entry_code entry_json="$(login entry1)" entry_token="$(extract_data_field "$entry_json" token)" entry_code="$(http_code -X POST "$API_URL/api/v1/cover-sheets/generate" \ -H "Authorization: Bearer $entry_token" \ -H 'Content-Type: application/json' \ -d '{"count":1,"batchType":"VITALS_SHEET","track":"BACKFILL"}')" if [[ "$entry_code" == "403" ]]; then pass "generate denied for DATA_ENTRY_CLERK (403)" else fail "generate denied for DATA_ENTRY_CLERK (403) — got HTTP $entry_code" fi local admin_json admin_json="$(generate_cover_sheets "$ADMIN_TOKEN" \ '{"count":1,"batchType":"LAB_RESULTS","track":"BACKFILL"}')" if [[ "$(jq -er '.success' <<<"$admin_json")" == "true" ]]; then pass "generate allowed for ADMINISTRATOR" else fail "generate allowed for ADMINISTRATOR" fi } test_cover_sheet_lookup_and_list() { section "3. Cover sheet lookup and list (plan §2)" if [[ -z "$UPLOAD_CODE" ]]; then fail "lookup requires generated cover sheet code" return fi local lookup_json is_used batch_type lookup_json="$(json_get "$API_URL/api/v1/cover-sheets/lookup/$UPLOAD_CODE" "$INTAKE_TOKEN")" if [[ "$(jq -er '.success' <<<"$lookup_json")" == "true" ]]; then pass "GET /cover-sheets/lookup/{code} returns success" else fail "GET /cover-sheets/lookup/{code} returns success" return fi is_used="$(extract_bool_field "$lookup_json" isUsed)" batch_type="$(extract_data_field "$lookup_json" batchType)" if [[ "$is_used" == "false" ]]; then pass "lookup shows isUsed=false before upload" else fail "lookup shows isUsed=false before upload (isUsed=$is_used)" fi if [[ "$batch_type" == "VITALS_SHEET" ]]; then pass "lookup returns batchType VITALS_SHEET" else fail "lookup returns batchType VITALS_SHEET (got $batch_type)" fi local unknown_json unknown_code unknown_json="$(json_get "$API_URL/api/v1/cover-sheets/lookup/VCR-CS-DEADBEEF" "$INTAKE_TOKEN")" unknown_code="$(extract_error_code "$unknown_json")" if [[ "$unknown_code" == "COVER_SHEET_NOT_FOUND" ]]; then pass "lookup unknown code returns COVER_SHEET_NOT_FOUND" else fail "lookup unknown code returns COVER_SHEET_NOT_FOUND (got ${unknown_code:-})" fi local list_json unused_count list_json="$(json_get "$API_URL/api/v1/cover-sheets?isUsed=false&page=1&pageSize=20" "$INTAKE_TOKEN")" unused_count="$(jq '.data | length' <<<"$list_json")" if [[ "$(jq -er '.success' <<<"$list_json")" == "true" && "$unused_count" -ge 5 ]]; then pass "GET /cover-sheets?isUsed=false lists unused sheets" else fail "GET /cover-sheets?isUsed=false lists unused sheets (count=$unused_count)" fi } test_pdf_generation() { section "4. Cover sheet PDF generation (plan Step 5)" if [[ -z "$UPLOAD_COVER_SHEET_ID" || ${#PDF_SHEET_IDS[@]} -lt 2 ]]; then fail "PDF tests require generated cover sheet IDs" return fi local pdf_tmp headers_file pdf_code content_type pdf_header pdf_tmp="$(mktemp)" headers_file="$(mktemp)" pdf_code="$(curl -sS -D "$headers_file" -o "$pdf_tmp" -w '%{http_code}' -X POST \ "$API_URL/api/v1/cover-sheets/${UPLOAD_COVER_SHEET_ID}/pdf" \ -H "Authorization: Bearer $INTAKE_TOKEN")" content_type="$(awk -F': ' 'tolower($1)=="content-type"{print $2}' "$headers_file" | tr -d '\r' | head -1)" rm -f "$headers_file" if [[ "$pdf_code" == "200" ]]; then pass "POST /cover-sheets/{id}/pdf returns 200" else fail "POST /cover-sheets/{id}/pdf returns 200 (got HTTP $pdf_code)" fi if [[ "$content_type" == application/pdf* ]]; then pass "single PDF response Content-Type is application/pdf" else fail "single PDF response Content-Type is application/pdf (got ${content_type:-})" fi pdf_header="$(head -c 8 "$pdf_tmp" || true)" if [[ "$pdf_header" == %PDF-1.* ]]; then pass "single PDF body starts with %PDF header" else fail "single PDF body starts with %PDF header" fi if grep -aq "$UPLOAD_CODE" "$pdf_tmp" 2>/dev/null; then pass "single PDF embeds cover sheet code text" else fail "single PDF embeds cover sheet code text" fi rm -f "$pdf_tmp" local batch_pdf_tmp batch_pdf_code batch_pdf_tmp="$(mktemp)" batch_pdf_code="$(curl -sS -o "$batch_pdf_tmp" -w '%{http_code}' -X POST \ "$API_URL/api/v1/cover-sheets/batch-pdf" \ -H "Authorization: Bearer $INTAKE_TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"coverSheetIds\":[\"${PDF_SHEET_IDS[0]}\",\"${PDF_SHEET_IDS[1]}\"]}")" if [[ "$batch_pdf_code" == "200" ]]; then pass "POST /cover-sheets/batch-pdf returns 200" else fail "POST /cover-sheets/batch-pdf returns 200 (got HTTP $batch_pdf_code)" fi if grep -aq '/Count 2' "$batch_pdf_tmp" 2>/dev/null || strings "$batch_pdf_tmp" | grep -q '/Count 2'; then pass "batch PDF contains two pages (/Count 2)" else fail "batch PDF contains two pages (/Count 2)" fi rm -f "$batch_pdf_tmp" } test_barcode_assisted_upload() { section "5. Barcode-assisted upload (plan §3)" if [[ -z "$UPLOAD_CODE" ]]; then fail "barcode upload requires generated cover sheet code" return fi local pdf upload_json batch_type track status entered_by pdf="$(unique_pdf_path upload)" upload_json="$(upload_with_cover_sheet "$INTAKE_TOKEN" "$UPLOAD_CODE" "$pdf")" rm -f "$pdf" if [[ "$(jq -er '.success' <<<"$upload_json")" == "true" ]]; then pass "POST /digitization-batches with coverSheetCode creates batch" else fail "POST /digitization-batches with coverSheetCode creates batch" log " response: $upload_json" return fi batch_type="$(extract_data_field "$upload_json" batchType)" track="$(extract_data_field "$upload_json" track)" status="$(extract_data_field "$upload_json" status)" UPLOAD_BATCH_ID="$(extract_data_field "$upload_json" id)" if [[ "$batch_type" == "VITALS_SHEET" && "$track" == "BACKFILL" ]]; then pass "batch inherits batchType and track from cover sheet" else fail "batch inherits batchType and track from cover sheet (type=$batch_type track=$track)" fi local redeemed_json redeemed_used redeemed_batch_id redeemed_json="$(json_get "$API_URL/api/v1/cover-sheets/lookup/$UPLOAD_CODE" "$INTAKE_TOKEN")" redeemed_used="$(extract_bool_field "$redeemed_json" isUsed)" redeemed_batch_id="$(extract_data_field "$redeemed_json" batchId)" if [[ "$redeemed_used" == "true" && "$redeemed_batch_id" == "$UPLOAD_BATCH_ID" ]]; then pass "cover sheet redeemed and linked to batch after upload" else fail "cover sheet redeemed and linked to batch after upload" fi if psql_available && [[ -n "$UPLOAD_COVER_SHEET_ID" ]]; then local db_used db_batch db_used="$(psql_query " SELECT is_used FROM cover_sheets WHERE id = '$UPLOAD_COVER_SHEET_ID'; ")" db_batch="$(psql_query " SELECT batch_id FROM cover_sheets WHERE id = '$UPLOAD_COVER_SHEET_ID'; ")" if [[ "$db_used" == "t" && "$db_batch" == "$UPLOAD_BATCH_ID" ]]; then pass "database row shows is_used=true with batch_id" else fail "database row shows is_used=true with batch_id" fi fi } test_cover_sheet_reuse_and_unknown() { section "6. Reuse prevention and unknown code (plan §4)" if [[ -z "$UPLOAD_CODE" ]]; then fail "reuse test requires uploaded cover sheet code" return fi local pdf reuse_response reuse_status reuse_code unknown_response unknown_status unknown_code pdf="$(unique_pdf_path reuse)" reuse_response="$(upload_with_cover_sheet_status "$INTAKE_TOKEN" "$UPLOAD_CODE" "$pdf")" rm -f "$pdf" reuse_status="${reuse_response##*__HTTP_STATUS__:}" reuse_response="${reuse_response%$'\n'__HTTP_STATUS__:*}" reuse_code="$(extract_error_code "$reuse_response")" if [[ "$reuse_status" == "409" && "$reuse_code" == "COVER_SHEET_ALREADY_USED" ]]; then pass "reused cover sheet code returns 409 COVER_SHEET_ALREADY_USED" else fail "reused cover sheet code returns 409 COVER_SHEET_ALREADY_USED (HTTP $reuse_status code=${reuse_code:-})" fi pdf="$(unique_pdf_path unknown)" unknown_response="$(upload_with_cover_sheet_status "$INTAKE_TOKEN" "VCR-CS-DEADBEEF" "$pdf")" rm -f "$pdf" unknown_status="${unknown_response##*__HTTP_STATUS__:}" unknown_response="${unknown_response%$'\n'__HTTP_STATUS__:*}" unknown_code="$(extract_error_code "$unknown_response")" if [[ "$unknown_status" == "404" && "$unknown_code" == "COVER_SHEET_NOT_FOUND" ]]; then pass "unknown cover sheet code on upload returns 404 COVER_SHEET_NOT_FOUND" else fail "unknown cover sheet code on upload returns 404 COVER_SHEET_NOT_FOUND (HTTP $unknown_status code=${unknown_code:-})" fi } test_auto_assign_cover_sheet() { section "7. Pre-assigned cover sheet auto-assigns batch" if [[ -z "$ENTRY_CLERK1_ID" ]]; then fail "auto-assign test skipped — no entry clerk ID" return fi local gen_json assign_code pdf upload_json status entered_by gen_json="$(generate_cover_sheets "$INTAKE_TOKEN" \ "{\"count\":1,\"batchType\":\"LAB_RESULTS\",\"track\":\"BACKFILL\",\"assignToUserId\":\"$ENTRY_CLERK1_ID\"}")" if [[ "$(jq -er '.success' <<<"$gen_json")" != "true" ]]; then fail "generate pre-assigned cover sheet" return fi assign_code="$(jq -r '.data[0].code' <<<"$gen_json")" pdf="$(unique_pdf_path assign)" upload_json="$(upload_with_cover_sheet "$INTAKE_TOKEN" "$assign_code" "$pdf")" rm -f "$pdf" status="$(extract_data_field "$upload_json" status)" entered_by="$(extract_data_field "$upload_json" enteredByUserId)" if [[ "$status" == "IN_ENTRY" ]]; then pass "pre-assigned upload transitions batch to IN_ENTRY" else fail "pre-assigned upload transitions batch to IN_ENTRY (status=$status)" fi if [[ "$entered_by" == "$ENTRY_CLERK1_ID" ]]; then pass "pre-assigned upload sets enteredByUserId to entry clerk" else fail "pre-assigned upload sets enteredByUserId to entry clerk (got $entered_by)" fi } test_patient_linked_cover_sheet() { section "8. Patient-linked cover sheet populates batch patient" if [[ -z "$PATIENT1_ID" ]]; then fail "patient-linked test skipped — no patient ID" return fi local gen_json patient_code pdf upload_json patient_id gen_json="$(generate_cover_sheets "$INTAKE_TOKEN" \ "{\"count\":1,\"batchType\":\"MEDICATION_LIST\",\"track\":\"BACKFILL\",\"patientId\":\"$PATIENT1_ID\"}")" if [[ "$(jq -er '.success' <<<"$gen_json")" != "true" ]]; then fail "generate patient-linked cover sheet" return fi patient_code="$(jq -r '.data[0].code' <<<"$gen_json")" pdf="$(unique_pdf_path patient)" upload_json="$(upload_with_cover_sheet "$INTAKE_TOKEN" "$patient_code" "$pdf")" rm -f "$pdf" patient_id="$(extract_data_field "$upload_json" patientId)" if [[ "$patient_id" == "$PATIENT1_ID" ]]; then pass "patient-linked cover sheet sets batch patientId" else fail "patient-linked cover sheet sets batch patientId (got $patient_id)" fi } test_integration_tests() { section "9. dotnet integration tests — CoverSheetBatchTests, CoverSheetPdfTests" 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~CoverSheet" \ --no-restore >/tmp/vigilcare-p10-tests.log 2>&1; then pass "CoverSheet integration tests passed" else fail "CoverSheet integration tests passed" log " see /tmp/vigilcare-p10-tests.log" fi } print_manual_ui_checklist() { section "10. Manual Vue UI checks (plan §5)" log " Login as intake1 → /cover-sheets" log " - Generate 5 VITALS_SHEET covers" log " - Print Cover Sheets opens PDF in new tab" log " - Cover sheet list shows unused sheets" log " Navigate to /intake" log " - Scan/type a cover sheet code (Enter triggers lookup)" log " - Lookup auto-fills batch type, track, patient" log " - Upload with Cover Sheet marks sheet as Used with linked batch ID" } main() { require_cmd curl require_cmd jq ensure_fixture_pdf log "VigilCare Records — Phase 10 verification" log "API: $API_URL" assert_api_reachable test_authentication test_database_schema test_cover_sheet_generation test_cover_sheet_lookup_and_list test_pdf_generation test_barcode_assisted_upload test_cover_sheet_reuse_and_unknown test_auto_assign_cover_sheet test_patient_linked_cover_sheet test_integration_tests 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 10 API verification checks passed." log "Complete the manual Vue UI checklist above if not already done." } main "$@"