247 lines
10 KiB
Bash
Executable File
247 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
BASE_URL="${1:-http://localhost:1202}"
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TMP_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
echo "curl is required for demo/export workflow verification" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "${PYTHON_BIN:-}" ]; then
|
|
PYTHON_BIN="${PYTHON_BIN}"
|
|
else
|
|
PYTHON_BIN=""
|
|
for candidate in python3 python.exe python; do
|
|
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import json, sys" >/dev/null 2>&1; then
|
|
PYTHON_BIN="${candidate}"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -z "${PYTHON_BIN}" ]; then
|
|
echo "A Python interpreter is required for JSON parsing" >&2
|
|
exit 1
|
|
fi
|
|
|
|
json_field() {
|
|
local file_path="$1"
|
|
local expression="$2"
|
|
"${PYTHON_BIN}" - "$file_path" "$expression" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
path, expression = sys.argv[1], sys.argv[2]
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
value = payload
|
|
for part in expression.split("."):
|
|
if part:
|
|
value = value[part]
|
|
print(value)
|
|
PY
|
|
}
|
|
|
|
require_json_data() {
|
|
local file_path="$1"
|
|
"${PYTHON_BIN}" - "$file_path" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
if "data" not in payload:
|
|
raise SystemExit("Response is not a canonical GeoIntel data envelope")
|
|
PY
|
|
}
|
|
|
|
echo "== GeoIntel demo/export workflow verification =="
|
|
echo "Base URL: ${BASE_URL}"
|
|
|
|
curl -fsS "${BASE_URL%/}/health" > "${TMP_DIR}/health.json"
|
|
if ! grep -q '"status":"ok"' "${TMP_DIR}/health.json"; then
|
|
echo "Health endpoint did not return ok:" >&2
|
|
cat "${TMP_DIR}/health.json" >&2
|
|
exit 1
|
|
fi
|
|
|
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/demo/workflow" > "${TMP_DIR}/demo.json"
|
|
require_json_data "${TMP_DIR}/demo.json"
|
|
project_id="$(json_field "${TMP_DIR}/demo.json" "data.project_id")"
|
|
candidate_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.candidate_dataset_id")"
|
|
reference_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.reference_dataset_id")"
|
|
area_id="$(json_field "${TMP_DIR}/demo.json" "data.area_id")"
|
|
quality_check_id="$(json_field "${TMP_DIR}/demo.json" "data.quality_check_id")"
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}" > "${TMP_DIR}/project.json"
|
|
require_json_data "${TMP_DIR}/project.json"
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/areas" > "${TMP_DIR}/areas.json"
|
|
require_json_data "${TMP_DIR}/areas.json"
|
|
"${PYTHON_BIN}" - "${TMP_DIR}/areas.json" "${area_id}" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
path, area_id = sys.argv[1], sys.argv[2]
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
items = payload["data"]["items"]
|
|
if not items:
|
|
raise SystemExit("Demo workflow did not expose any project areas")
|
|
matches = [item for item in items if item["id"] == area_id]
|
|
if not matches:
|
|
raise SystemExit(f"Demo area {area_id} was not returned by the area list")
|
|
geometry = matches[0].get("geometry")
|
|
if not geometry or geometry.get("type") not in {"Polygon", "MultiPolygon"}:
|
|
raise SystemExit("Demo area response does not include GeoJSON Polygon/MultiPolygon geometry")
|
|
PY
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets" > "${TMP_DIR}/datasets.json"
|
|
require_json_data "${TMP_DIR}/datasets.json"
|
|
"${PYTHON_BIN}" - "${TMP_DIR}/datasets.json" "${candidate_dataset_id}" "${reference_dataset_id}" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
path, candidate_id, reference_id = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
items = payload["data"]["items"]
|
|
ids = {item["id"]: item for item in items}
|
|
if candidate_id not in ids:
|
|
raise SystemExit("Candidate fixture dataset is missing from project dataset list")
|
|
if reference_id not in ids:
|
|
raise SystemExit("Reference fixture dataset is missing from project dataset list")
|
|
if ids[candidate_id]["dataset_type"] not in {"vector", "geojson"}:
|
|
raise SystemExit("Candidate fixture dataset is not vector-like")
|
|
if ids[reference_id].get("dataset_role") != "reference":
|
|
raise SystemExit("Reference fixture dataset is not marked as reference")
|
|
PY
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets/${candidate_dataset_id}/content" > "${TMP_DIR}/candidate_content.json"
|
|
if ! grep -q '"FeatureCollection"' "${TMP_DIR}/candidate_content.json"; then
|
|
echo "Candidate dataset content is not a FeatureCollection" >&2
|
|
exit 1
|
|
fi
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/datasets/${candidate_dataset_id}/vector/summary" > "${TMP_DIR}/candidate_summary.json"
|
|
require_json_data "${TMP_DIR}/candidate_summary.json"
|
|
"${PYTHON_BIN}" - "${TMP_DIR}/candidate_summary.json" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
feature_count = payload["data"].get("feature_count") or 0
|
|
if feature_count < 1:
|
|
raise SystemExit("Candidate vector summary does not report persisted features")
|
|
PY
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/projects/${project_id}/quality-checks" > "${TMP_DIR}/quality_checks.json"
|
|
require_json_data "${TMP_DIR}/quality_checks.json"
|
|
quality_count="$(json_field "${TMP_DIR}/quality_checks.json" "data.total")"
|
|
if [ "${quality_count}" -lt 1 ]; then
|
|
echo "Expected at least one persisted QA/QC result after demo workflow" >&2
|
|
exit 1
|
|
fi
|
|
"${PYTHON_BIN}" - "${TMP_DIR}/quality_checks.json" "${quality_check_id}" "${ROOT}/fixtures/golden/expected_qa_metrics.json" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
path, quality_check_id, expected_path = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
with open(expected_path, "r", encoding="utf-8") as handle:
|
|
expected = json.load(handle)
|
|
items = payload["data"]["items"]
|
|
matches = [item for item in items if item["id"] == quality_check_id]
|
|
if not matches:
|
|
raise SystemExit("Seeded quality_check_id is not listed in project QA/QC results")
|
|
quality_check = matches[0]
|
|
if quality_check.get("status") != "ok":
|
|
raise SystemExit(f"Seeded QA/QC result has unexpected status: {quality_check.get('status')}")
|
|
if abs(float(quality_check.get("score")) - float(expected["f1"])) > float(expected["tolerance"]):
|
|
raise SystemExit("Seeded QA/QC score does not match the golden F1 baseline")
|
|
metrics = {metric["metric_key"]: metric.get("metric_value") for metric in quality_check.get("metrics", [])}
|
|
required = {
|
|
"precision": expected["precision"],
|
|
"recall": expected["recall"],
|
|
"f1": expected["f1"],
|
|
"mean_iou": expected["mean_iou"],
|
|
"false_positive_count": expected["false_positive_count"],
|
|
"false_negative_count": expected["false_negative_count"],
|
|
}
|
|
missing = set(required) - set(metrics)
|
|
if missing:
|
|
raise SystemExit(f"QA/QC result is missing metrics: {sorted(missing)}")
|
|
for key, expected_value in required.items():
|
|
actual = metrics[key]
|
|
if actual is None:
|
|
raise SystemExit(f"QA/QC metric {key} is None")
|
|
if abs(float(actual) - float(expected_value)) > float(expected["tolerance"]):
|
|
raise SystemExit(
|
|
f"QA/QC metric {key} drifted: actual={actual}, expected={expected_value}, tolerance={expected['tolerance']}"
|
|
)
|
|
findings = quality_check.get("findings_json") or {}
|
|
if int(findings.get("matches", -1)) != int(expected["matches"]):
|
|
raise SystemExit("Seeded QA/QC match count does not match golden baseline")
|
|
if int(findings.get("false_positives", -1)) != int(expected["false_positive_count"]):
|
|
raise SystemExit("Seeded QA/QC false-positive count does not match golden baseline")
|
|
if int(findings.get("false_negatives", -1)) != int(expected["false_negative_count"]):
|
|
raise SystemExit("Seeded QA/QC false-negative count does not match golden baseline")
|
|
PY
|
|
|
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/metadata" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"project_id\":\"${project_id}\"}" > "${TMP_DIR}/metadata_export.json"
|
|
require_json_data "${TMP_DIR}/metadata_export.json"
|
|
metadata_export_id="$(json_field "${TMP_DIR}/metadata_export.json" "data.export_id")"
|
|
|
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/report" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"project_id\":\"${project_id}\"}" > "${TMP_DIR}/report_export.json"
|
|
require_json_data "${TMP_DIR}/report_export.json"
|
|
report_export_id="$(json_field "${TMP_DIR}/report_export.json" "data.export_id")"
|
|
|
|
curl -fsS -X POST "${BASE_URL%/}/api/v1/exports/geojson" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"export_kind\":\"dataset\",\"dataset_id\":\"${candidate_dataset_id}\"}" > "${TMP_DIR}/dataset_export.json"
|
|
require_json_data "${TMP_DIR}/dataset_export.json"
|
|
dataset_export_id="$(json_field "${TMP_DIR}/dataset_export.json" "data.export_id")"
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/exports/projects/${project_id}/exports" > "${TMP_DIR}/exports.json"
|
|
require_json_data "${TMP_DIR}/exports.json"
|
|
export_count="$(json_field "${TMP_DIR}/exports.json" "data.total")"
|
|
if [ "${export_count}" -lt 3 ]; then
|
|
echo "Expected at least three exports after workflow smoke, found ${export_count}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/exports/${metadata_export_id}/content" > "${TMP_DIR}/metadata_content.json"
|
|
require_json_data "${TMP_DIR}/metadata_content.json"
|
|
if ! grep -q '"quality_checks"' "${TMP_DIR}/metadata_content.json"; then
|
|
echo "Metadata export content does not include QA/QC summary" >&2
|
|
exit 1
|
|
fi
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/exports/${dataset_export_id}/download" > "${TMP_DIR}/dataset_download.geojson"
|
|
if ! grep -q '"FeatureCollection"' "${TMP_DIR}/dataset_download.geojson"; then
|
|
echo "Dataset GeoJSON download is not a FeatureCollection" >&2
|
|
exit 1
|
|
fi
|
|
|
|
curl -fsS "${BASE_URL%/}/api/v1/exports/${report_export_id}/download" > "${TMP_DIR}/report_download.html"
|
|
if ! grep -qi '<!doctype html>' "${TMP_DIR}/report_download.html"; then
|
|
echo "Report download is not an HTML artifact" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Demo/export workflow verification passed"
|
|
echo "Project: ${project_id}"
|
|
echo "Area: ${area_id}"
|
|
echo "Datasets: candidate=${candidate_dataset_id}, reference=${reference_dataset_id}"
|
|
echo "Exports created: metadata=${metadata_export_id}, report=${report_export_id}, dataset=${dataset_export_id}"
|