feature: Optional OCR-Assisted Draft Pre-Fill
This commit is contained in:
+670
@@ -0,0 +1,670 @@
|
||||
#!/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 1–12 seed data (intake1, entry1)
|
||||
#
|
||||
# Optional live OCR checks (plan §4–6):
|
||||
# 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 §4–6)"
|
||||
|
||||
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 "$@"
|
||||
Reference in New Issue
Block a user