Files
vigilcare-records/scripts/run-vigilcare-records-phase-13-verification.sh
T
voltsrage c871dc4842
CI / backend (push) Failing after 2m26s
CI / frontend (push) Failing after 53s
Add deployment files
2026-08-11 20:17:53 +08:00

671 lines
20 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Runs Phase 13 verification checks from docs/plans/phase-13-plan.md.
#
# Covers OCR schema/config, draft ocrConfidence API field, manual-entry
# non-interference, and optional live OCR polling when the API runs with OCR enabled.
#
# Prerequisites:
# docker compose up -d (PostgreSQL, Redis, MinIO)
# dotnet ef database update --project VigilCareRecordsAPI
# dotnet run --project VigilCareRecordsAPI (Ocr:Enabled=false by default)
# Phase 112 seed data (intake1, entry1)
#
# Optional live OCR checks (plan §46):
# Restart API with OCR enabled, e.g.:
# Ocr__Enabled=true Ocr__Provider=tesseract dotnet run --project VigilCareRecordsAPI
# Ensure Tesseract data is installed (e.g. /usr/share/tessdata/eng.traineddata)
# VIGILCARE_OCR_LIVE=1 ./scripts/run-vigilcare-records-phase-13-verification.sh
#
# Environment overrides:
# VIGILCARE_API_URL default: http://localhost:5217
# VIGILCARE_COMPOSE_FILE default: <repo>/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_BUILD_CHECKS set to 1 to skip dotnet build/test
# VIGILCARE_SKIP_API_CHECKS set to 1 to skip HTTP API checks
# VIGILCARE_OCR_LIVE set to 1 to run live OCR polling tests
# VIGILCARE_OCR_POLL_WAIT_SEC default: 20 (should exceed Ocr:PollIntervalSeconds)
#
# Usage:
# chmod +x scripts/run-vigilcare-records-phase-13-verification.sh
# ./scripts/run-vigilcare-records-phase-13-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"
APPSETTINGS="$REPO_ROOT/VigilCareRecordsAPI/appsettings.json"
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_BUILD_CHECKS="${VIGILCARE_SKIP_BUILD_CHECKS:-0}"
SKIP_API_CHECKS="${VIGILCARE_SKIP_API_CHECKS:-0}"
OCR_LIVE="${VIGILCARE_OCR_LIVE:-0}"
OCR_POLL_WAIT_SEC="${VIGILCARE_OCR_POLL_WAIT_SEC:-20}"
INTAKE_TOKEN=""
ENTRY_TOKEN=""
ENTRY_CLERK1_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
}
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<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R/Resources<<>>>>endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
206
%%EOF
EOF
}
unique_pdf_path() {
local suffix="$1"
local path="/tmp/vigilcare-p13-${suffix}-${RANDOM}.pdf"
cp "$FIXTURE_PDF" "$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"
}
json_put() {
local url="$1"
local body="$2"
local token="$3"
curl -sS -X PUT "$url" \
-H "Authorization: Bearer $token" \
-H 'Content-Type: application/json' \
-d "$body"
}
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"
}
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_batch() {
local token="$1"
local pdf="$2"
local batch_type="${3:-VITALS_SHEET}"
local track="${4:-BACKFILL}"
curl -sS -X POST "$API_URL/api/v1/digitization-batches" \
-H "Authorization: Bearer $token" \
-F "file=@${pdf};type=application/pdf" \
-F "batchType=$batch_type" \
-F "track=$track"
}
assign_batch() {
local token="$1"
local batch_id="$2"
local entry_clerk_id="$3"
curl -sS -X PATCH "$API_URL/api/v1/digitization-batches/$batch_id/assign" \
-H "Authorization: Bearer $token" \
-H 'Content-Type: application/json' \
-d "{\"entryClerkId\":\"$entry_clerk_id\"}"
}
test_build_and_unit_tests() {
section "1. Backend compiles and tests pass (plan §1)"
if [[ "$SKIP_BUILD_CHECKS" == "1" ]]; then
log " SKIP: dotnet build/test (VIGILCARE_SKIP_BUILD_CHECKS=1)"
return
fi
if ! command -v dotnet >/dev/null 2>&1; then
fail "dotnet SDK available for build"
return
fi
if dotnet build "$REPO_ROOT/VigilCareRecordsAPI/VigilCareRecordsAPI.csproj" \
--nologo -v q >/tmp/vigilcare-p13-build.log 2>&1; then
pass "dotnet build succeeds"
else
fail "dotnet build succeeds"
log " see /tmp/vigilcare-p13-build.log"
return
fi
if dotnet test "$REPO_ROOT/VigilCareRecordsAPI.Tests/VigilCareRecordsAPI.Tests.csproj" \
--no-build --nologo >/tmp/vigilcare-p13-tests.log 2>&1; then
pass "dotnet test passes"
else
fail "dotnet test passes"
log " see /tmp/vigilcare-p13-tests.log"
fi
}
test_database_schema() {
section "2. Migration — ocr_results and OCR event types (plan §2, §4)"
if ! psql_available; then
log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)"
return
fi
local table_exists unique_batch constraint_ok migration_ok
table_exists="$(psql_query "
SELECT count(*)
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'ocr_results';
")"
unique_batch="$(psql_query "
SELECT count(*)
FROM pg_indexes
WHERE tablename = 'ocr_results' AND indexdef LIKE '%UNIQUE%' AND indexdef LIKE '%batch_id%';
")"
constraint_ok="$(psql_query "
SELECT count(*)
FROM pg_constraint c
JOIN pg_class t ON c.conrelid = t.oid
WHERE t.relname = 'digitization_events'
AND c.conname = 'chk_digitization_events_event_type'
AND pg_get_constraintdef(c.oid) LIKE '%ocr_started%'
AND pg_get_constraintdef(c.oid) LIKE '%ocr_completed%'
AND pg_get_constraintdef(c.oid) LIKE '%ocr_failed%';
")"
migration_ok="$(psql_query "
SELECT count(*)
FROM public.\"__EFMigrationsHistory\"
WHERE \"MigrationId\" LIKE '%AddOcrResult%';
" 2>/dev/null || echo "0")"
if [[ "$table_exists" == "1" ]]; then
pass "ocr_results table exists"
else
fail "ocr_results table exists (run: dotnet ef database update --project VigilCareRecordsAPI)"
fi
if [[ "$unique_batch" == "1" ]]; then
pass "unique index on ocr_results.batch_id exists"
else
fail "unique index on ocr_results.batch_id exists"
fi
if [[ "$constraint_ok" == "1" ]]; then
pass "digitization_events check constraint includes OCR event types"
else
fail "digitization_events check constraint includes OCR event types"
fi
if [[ "$migration_ok" == "1" ]]; then
pass "AddOcrResult migration applied"
else
fail "AddOcrResult migration applied"
fi
}
test_ocr_disabled_by_default_config() {
section "3. OCR disabled by default — appsettings (plan §3)"
if [[ ! -f "$APPSETTINGS" ]]; then
fail "appsettings.json exists"
return
fi
local enabled provider
enabled="$(jq -er '.Ocr.Enabled' <<<"$(cat "$APPSETTINGS")")"
provider="$(jq -er '.Ocr.Provider // empty' <<<"$(cat "$APPSETTINGS")")"
if [[ "$enabled" == "false" ]]; then
pass "appsettings Ocr.Enabled is false by default"
else
fail "appsettings Ocr.Enabled is false by default (got: $enabled)"
fi
if [[ -n "$provider" ]]; then
pass "appsettings defines Ocr.Provider"
else
fail "appsettings defines Ocr.Provider"
fi
if [[ -f "$REPO_ROOT/VigilCareRecordsAPI/BackgroundServices/OcrProcessingService.cs" ]]; then
pass "OcrProcessingService source present"
else
fail "OcrProcessingService source present"
fi
if grep -q 'OcrConfidenceMap' "$REPO_ROOT/VigilCareRecordsAPI/Models/Records/Batch/DraftPayloadResponse.cs" 2>/dev/null; then
pass "DraftPayloadResponse includes OcrConfidenceMap"
else
fail "DraftPayloadResponse includes OcrConfidenceMap"
fi
}
test_authentication() {
section "4. Authentication"
local intake_json entry_json users_json
intake_json="$(login intake1)"
entry_json="$(login entry1)"
INTAKE_TOKEN="$(extract_data_field "$intake_json" token)"
ENTRY_TOKEN="$(extract_data_field "$entry_json" token)"
if [[ -n "$INTAKE_TOKEN" ]]; then
pass "intake1 login returns JWT"
else
fail "intake1 login returns JWT"
fi
if [[ -n "$ENTRY_TOKEN" ]]; then
pass "entry1 login returns JWT"
else
fail "entry1 login returns JWT"
fi
users_json="$(json_get "$API_URL/api/v1/users?role=DATA_ENTRY_CLERK" "$INTAKE_TOKEN")"
ENTRY_CLERK1_ID="$(jq -er '.data[0].id // empty' <<<"$users_json" 2>/dev/null || true)"
if [[ -n "$ENTRY_CLERK1_ID" ]]; then
pass "resolved entry clerk ID for assign test"
else
fail "resolved entry clerk ID for assign test"
fi
}
test_draft_ocr_confidence_null_when_ocr_off() {
section "5. Draft API — ocrConfidence null without OCR run (plan §3, §8)"
if [[ -z "$INTAKE_TOKEN" ]]; then
fail "draft ocrConfidence test skipped — no intake token"
return
fi
local pdf upload_json batch_id draft_json ocr_conf
pdf="$(unique_pdf_path draft-null)"
upload_json="$(upload_batch "$INTAKE_TOKEN" "$pdf")"
rm -f "$pdf"
batch_id="$(extract_data_field "$upload_json" id)"
if [[ -z "$batch_id" ]]; then
fail "upload batch for draft ocrConfidence test"
return
fi
draft_json="$(json_get "$API_URL/api/v1/digitization-batches/$batch_id/draft" "$ENTRY_TOKEN")"
if jq -e '.success == true' <<<"$draft_json" >/dev/null 2>&1; then
pass "GET /draft returns success for UPLOADED batch"
else
fail "GET /draft returns success for UPLOADED batch"
return
fi
if jq -e '.data | has("ocrConfidence")' <<<"$draft_json" >/dev/null 2>&1; then
pass "draft payload includes ocrConfidence field"
else
fail "draft payload includes ocrConfidence field"
fi
ocr_conf="$(jq -r '.data.ocrConfidence // "missing"' <<<"$draft_json")"
if [[ "$ocr_conf" == "null" || "$ocr_conf" == "missing" ]]; then
pass "ocrConfidence is null when OCR has not processed batch"
else
fail "ocrConfidence is null when OCR has not processed batch (got: $ocr_conf)"
fi
local status
status="$(extract_data_field "$upload_json" status)"
if [[ "$status" == "UPLOADED" ]]; then
pass "uploaded batch remains UPLOADED before entry"
else
fail "uploaded batch remains UPLOADED before entry (status=$status)"
fi
}
test_manual_entry_skips_ocr() {
section "6. No interference with manual entry (plan §7)"
if [[ -z "$INTAKE_TOKEN" || -z "$ENTRY_TOKEN" || -z "$ENTRY_CLERK1_ID" ]]; then
fail "manual-entry interference test skipped — missing auth IDs"
return
fi
local pdf upload_json batch_id assign_json put_json status ocr_row event_count
pdf="$(unique_pdf_path manual-entry)"
upload_json="$(upload_batch "$INTAKE_TOKEN" "$pdf")"
rm -f "$pdf"
batch_id="$(extract_data_field "$upload_json" id)"
if [[ -z "$batch_id" ]]; then
fail "upload batch for manual-entry test"
return
fi
assign_json="$(assign_batch "$INTAKE_TOKEN" "$batch_id" "$ENTRY_CLERK1_ID")"
if [[ "$(jq -er '.success // false' <<<"$assign_json")" == "true" ]]; then
pass "batch assigned to entry clerk"
else
fail "batch assigned to entry clerk"
return
fi
put_json="$(json_put \
"$API_URL/api/v1/digitization-batches/$batch_id/draft/patient" \
'{"fullName":"Manual Entry Patient","dateOfBirth":"1990-01-15","sex":"female"}' \
"$ENTRY_TOKEN")"
if [[ "$(jq -er '.success // false' <<<"$put_json")" == "true" ]]; then
pass "entry clerk saves draft patient before OCR can run"
else
fail "entry clerk saves draft patient before OCR can run"
return
fi
status="$(json_get "$API_URL/api/v1/digitization-batches/$batch_id" "$ENTRY_TOKEN" \
| jq -r '.data.status // empty')"
if [[ "$status" == "IN_ENTRY" ]]; then
pass "batch transitions to IN_ENTRY after first draft save"
else
fail "batch transitions to IN_ENTRY after first draft save (status=$status)"
fi
if psql_available; then
ocr_row="$(psql_query "
SELECT count(*) FROM ocr_results WHERE batch_id = '$batch_id';
")"
event_count="$(psql_query "
SELECT count(*)
FROM digitization_events
WHERE batch_id = '$batch_id'
AND event_type IN ('ocr_started', 'ocr_completed', 'ocr_failed');
")"
if [[ "$ocr_row" == "0" ]]; then
pass "no ocr_results row for IN_ENTRY batch"
else
fail "no ocr_results row for IN_ENTRY batch (count=$ocr_row)"
fi
if [[ "$event_count" == "0" ]]; then
pass "no OCR lifecycle events for IN_ENTRY batch"
else
fail "no OCR lifecycle events for IN_ENTRY batch (count=$event_count)"
fi
else
log " SKIP: DB assertions for manual-entry test (postgres unavailable)"
fi
}
test_live_ocr_polling() {
section "7. Live OCR polling (optional — plan §46)"
if [[ "$OCR_LIVE" != "1" ]]; then
log " SKIP: live OCR tests (set VIGILCARE_OCR_LIVE=1 and restart API with Ocr__Enabled=true)"
log " Example: Ocr__Enabled=true Ocr__Provider=tesseract dotnet run --project VigilCareRecordsAPI"
return
fi
if [[ -z "$INTAKE_TOKEN" ]]; then
fail "live OCR test skipped — no intake token"
return
fi
if ! psql_available; then
fail "live OCR test requires PostgreSQL for event/result assertions"
return
fi
local pdf upload_json batch_id draft_json provider ocr_conf event_completed event_failed
pdf="$(unique_pdf_path ocr-live)"
upload_json="$(upload_batch "$INTAKE_TOKEN" "$pdf")"
rm -f "$pdf"
batch_id="$(extract_data_field "$upload_json" id)"
if [[ -z "$batch_id" ]]; then
fail "upload batch for live OCR test"
return
fi
log " Waiting ${OCR_POLL_WAIT_SEC}s for OcrProcessingService poll cycle..."
sleep "$OCR_POLL_WAIT_SEC"
event_completed="$(psql_query "
SELECT count(*) FROM digitization_events
WHERE batch_id = '$batch_id' AND event_type = 'ocr_completed';
")"
event_failed="$(psql_query "
SELECT count(*) FROM digitization_events
WHERE batch_id = '$batch_id' AND event_type = 'ocr_failed';
")"
local ocr_started
ocr_started="$(psql_query "
SELECT count(*) FROM digitization_events
WHERE batch_id = '$batch_id' AND event_type = 'ocr_started';
")"
if [[ "$ocr_started" -ge 1 ]]; then
pass "OcrStarted event written for uploaded batch"
else
fail "OcrStarted event written for uploaded batch (is API running with Ocr__Enabled=true?)"
fi
if [[ "$event_completed" == "1" ]]; then
pass "OcrCompleted event written"
elif [[ "$event_failed" == "1" ]]; then
pass "OcrFailed event written (OCR failure is non-blocking — plan §6)"
local batch_status
batch_status="$(psql_query "
SELECT status FROM digitization_batches WHERE id = '$batch_id';
")"
if [[ "$batch_status" == "UPLOADED" ]]; then
pass "batch remains UPLOADED after OCR failure"
else
fail "batch remains UPLOADED after OCR failure (status=$batch_status)"
fi
return
else
fail "OcrCompleted or OcrFailed event written after poll wait"
return
fi
draft_json="$(json_get "$API_URL/api/v1/digitization-batches/$batch_id/draft" "$ENTRY_TOKEN")"
ocr_conf="$(jq -c '.data.ocrConfidence // null' <<<"$draft_json")"
provider="$(jq -r '.data.ocrConfidence.provider // empty' <<<"$draft_json")"
if [[ "$ocr_conf" != "null" && -n "$provider" ]]; then
pass "GET /draft returns ocrConfidence with provider ($provider)"
else
fail "GET /draft returns ocrConfidence with provider (got: $ocr_conf)"
fi
if jq -e '.data.ocrConfidence.fieldConfidences | type == "object"' <<<"$draft_json" >/dev/null 2>&1; then
pass "ocrConfidence.fieldConfidences is an object"
else
fail "ocrConfidence.fieldConfidences is an object"
fi
local ocr_row
ocr_row="$(psql_query "
SELECT count(*) FROM ocr_results WHERE batch_id = '$batch_id';
")"
if [[ "$ocr_row" == "1" ]]; then
pass "ocr_results row persisted for batch"
else
fail "ocr_results row persisted for batch"
fi
}
print_manual_ui_checklist() {
section "8. Manual Vue UI checks (plan §8)"
log " Login as entry1 → open an OCR-processed batch in the entry form"
log " - Blue OCR banner shows provider name"
log " - Pre-filled fields show green/yellow/red left border by confidence"
log " - Fields below confidence threshold remain empty"
log " Edit a pre-filled field → save → confirm draft_field_updated audit event"
log " Login as verifier → open batch in verification form"
log " - OCR banner and confidence borders visible in read-only mode"
log " OCR disabled (default): confirm no OcrProcessingService started in API logs"
log " Azure provider: restart with Ocr__Provider=azure and valid credentials (plan §5)"
log " OCR failure: restart with invalid Azure endpoint → OcrFailed logged, manual entry works (plan §6)"
}
main() {
require_cmd curl
require_cmd jq
ensure_fixture_pdf
log "VigilCare Records — Phase 13 verification (Optional OCR-Assisted Draft Pre-Fill)"
log "API: $API_URL"
log "OCR live tests: $([[ "$OCR_LIVE" == "1" ]] && echo enabled || echo disabled)"
test_build_and_unit_tests
test_database_schema
test_ocr_disabled_by_default_config
if [[ "$SKIP_API_CHECKS" == "1" ]]; then
log ""
log "SKIP: HTTP API checks (VIGILCARE_SKIP_API_CHECKS=1)"
else
assert_api_reachable
test_authentication
test_draft_ocr_confidence_null_when_ocr_off
test_manual_entry_skips_ocr
test_live_ocr_polling
fi
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 13 automated verification checks passed."
log "Complete the manual Vue UI checklist above if not already done."
if [[ "$OCR_LIVE" != "1" ]]; then
log "For live OCR polling tests, restart the API with OCR enabled and re-run with VIGILCARE_OCR_LIVE=1."
fi
}
main "$@"