#!/usr/bin/env bash set -euo pipefail BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "${TMP_DIR}"' EXIT if ! command -v curl >/dev/null 2>&1; then echo "curl is required for model asset detection 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 model asset detection workflow verification ==" echo "Base URL: ${BASE_URL}" 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")" raster_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.raster_dataset_id")" if [ -z "${project_id}" ] || [ "${project_id}" = "None" ] || [ "${project_id}" = "null" ]; then echo "Demo workflow did not return a project_id" >&2 exit 1 fi if [ -z "${raster_dataset_id}" ] || [ "${raster_dataset_id}" = "None" ] || [ "${raster_dataset_id}" = "null" ]; then echo "Demo workflow did not return a raster_dataset_id" >&2 exit 1 fi curl -fsS -X POST "${BASE_URL%/}/api/v1/projects/${project_id}/datasets/${raster_dataset_id}/raster/tile" \ -H "Content-Type: application/json" \ -d '{"tile_size":64,"overlap":0,"output_name":"model_asset_detection_smoke_tiles"}' > "${TMP_DIR}/tile.json" require_json_data "${TMP_DIR}/tile.json" manifest_path="$(json_field "${TMP_DIR}/tile.json" "data.result_json.manifest_path")" if [ -z "${manifest_path}" ] || [ "${manifest_path}" = "None" ] || [ "${manifest_path}" = "null" ]; then echo "Raster tile response did not include a manifest_path" >&2 exit 1 fi curl -fsS "${BASE_URL%/}/api/v1/detection/model-assets" > "${TMP_DIR}/model_assets.json" require_json_data "${TMP_DIR}/model_assets.json" model_asset_id="$("${PYTHON_BIN}" - "${TMP_DIR}/model_assets.json" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8") as handle: data = json.load(handle)["data"] items = data.get("items") or [] if not items: raise SystemExit("No local model assets are available. Mount a local .pt/.onnx/.engine file before running this smoke.") for item in items: if item.get("will_download_models") is not False: raise SystemExit("Model asset catalog must never report automatic model downloads") selected = next((item for item in items if item.get("active")), items[0]) print(selected["model_asset_id"]) PY )" curl -fsS -G "${BASE_URL%/}/api/v1/detection/yolo/preflight" \ --data-urlencode "tile_manifest_path=${manifest_path}" \ --data-urlencode "model_asset_id=${model_asset_id}" > "${TMP_DIR}/preflight.json" require_json_data "${TMP_DIR}/preflight.json" "${PYTHON_BIN}" - "${TMP_DIR}/preflight.json" "${model_asset_id}" <<'PY' import json import sys path, expected_model_asset_id = sys.argv[1], sys.argv[2] with open(path, "r", encoding="utf-8") as handle: data = json.load(handle)["data"] if data.get("model_asset_id") != expected_model_asset_id: raise SystemExit("YOLO preflight did not use the selected model_asset_id") if data.get("will_download_models") is not False: raise SystemExit("YOLO preflight must never download model weights") if data.get("will_run_inference") is not False: raise SystemExit("YOLO preflight must remain read-only") if data.get("status") != "ready": raise SystemExit(f"YOLO preflight is not ready: {data.get('status')} {data.get('message')}") checks = data.get("checks") or {} if checks.get("manifest_valid") is not True: raise SystemExit("YOLO preflight did not validate the raster tile manifest") if checks.get("model_file_exists") is not True: raise SystemExit("YOLO preflight did not confirm the local model file") PY "${PYTHON_BIN}" - "${TMP_DIR}/run_request.json" "${project_id}" "${raster_dataset_id}" "${model_asset_id}" "${manifest_path}" <<'PY' import json import sys path, project_id, dataset_id, model_asset_id, tile_manifest_path = sys.argv[1:6] payload = { "project_id": project_id, "dataset_id": dataset_id, "model_id": "yolo-configured", "model_asset_id": model_asset_id, "confidence_threshold": 0.5, "tile_manifest_path": tile_manifest_path, "parameters_json": {}, } with open(path, "w", encoding="utf-8") as handle: json.dump(payload, handle) PY curl -fsS -X POST "${BASE_URL%/}/api/v1/detection/run" \ -H "Content-Type: application/json" \ --data-binary "@${TMP_DIR}/run_request.json" > "${TMP_DIR}/detection_run.json" require_json_data "${TMP_DIR}/detection_run.json" "${PYTHON_BIN}" - "${TMP_DIR}/detection_run.json" "${model_asset_id}" <<'PY' import json import sys path, expected_model_asset_id = sys.argv[1], sys.argv[2] with open(path, "r", encoding="utf-8") as handle: data = json.load(handle)["data"] if data.get("model_id") != "yolo-configured": raise SystemExit("Detection run did not use yolo-configured") if not data.get("analysis_run_id") or not data.get("job_id"): raise SystemExit("Detection run did not return persisted run/job ids") if data.get("status") != "success": raise SystemExit(f"Detection run failed: {data.get('error_code')} {data.get('message')}") if int(data.get("detection_count") or 0) < 0: raise SystemExit("Detection count cannot be negative") PY analysis_run_id="$(json_field "${TMP_DIR}/detection_run.json" "data.analysis_run_id")" detection_count="$(json_field "${TMP_DIR}/detection_run.json" "data.detection_count")" curl -fsS "${BASE_URL%/}/api/v1/detection/runs/${analysis_run_id}" > "${TMP_DIR}/run_detail.json" require_json_data "${TMP_DIR}/run_detail.json" "${PYTHON_BIN}" - "${TMP_DIR}/run_detail.json" "${analysis_run_id}" "${model_asset_id}" "${manifest_path}" <<'PY' import json import sys path, analysis_run_id, model_asset_id, tile_manifest_path = sys.argv[1:5] with open(path, "r", encoding="utf-8") as handle: data = json.load(handle)["data"] if data.get("id") != analysis_run_id: raise SystemExit("Detection run detail returned the wrong run id") if data.get("status") != "success": raise SystemExit(f"Detection run detail is not successful: {data.get('status')}") parameters = data.get("parameters_json") or {} if parameters.get("model_asset_id") != model_asset_id: raise SystemExit("Persisted run parameters lost model_asset_id provenance") if parameters.get("tile_manifest_path") != tile_manifest_path: raise SystemExit("Persisted run parameters lost tile_manifest_path provenance") PY curl -fsS "${BASE_URL%/}/api/v1/detection/runs/${analysis_run_id}/detections" > "${TMP_DIR}/detections.json" require_json_data "${TMP_DIR}/detections.json" "${PYTHON_BIN}" - "${TMP_DIR}/detections.json" "${detection_count}" <<'PY' import json import sys path, expected_count = sys.argv[1], int(sys.argv[2]) with open(path, "r", encoding="utf-8") as handle: data = json.load(handle)["data"] items = data.get("items") or [] if int(data.get("total") or 0) != expected_count: raise SystemExit("Detection list total does not match run detection_count") if len(items) != expected_count: raise SystemExit("Detection list item count does not match run detection_count") for item in items: if not item.get("geometry"): raise SystemExit("Persisted detection is missing geometry") if not item.get("source_tile_path"): raise SystemExit("Persisted YOLO detection is missing source_tile_path provenance") PY curl -fsS "${BASE_URL%/}/api/v1/detection/runs/${analysis_run_id}/geojson" > "${TMP_DIR}/geojson.json" require_json_data "${TMP_DIR}/geojson.json" "${PYTHON_BIN}" - "${TMP_DIR}/geojson.json" "${detection_count}" <<'PY' import json import sys path, expected_count = sys.argv[1], int(sys.argv[2]) with open(path, "r", encoding="utf-8") as handle: data = json.load(handle)["data"] if data.get("type") != "FeatureCollection": raise SystemExit("Detection GeoJSON response is not a FeatureCollection") features = data.get("features") or [] if len(features) != expected_count: raise SystemExit("Detection GeoJSON feature count does not match run detection_count") required_properties = { "detection_id", "class_name", "confidence", "model_name", "model_version", "analysis_run_id", "dataset_id", "job_id", "source_tile_path", "bbox_json", } for feature in features: properties = feature.get("properties") or {} missing = sorted(required_properties - set(properties)) if missing: raise SystemExit(f"Detection GeoJSON feature is missing properties: {missing}") if not feature.get("geometry"): raise SystemExit("Detection GeoJSON feature is missing geometry") PY echo "Model asset detection workflow verification passed" echo "Project: ${project_id}" echo "Raster dataset: ${raster_dataset_id}" echo "Model asset: ${model_asset_id}" echo "Manifest: ${manifest_path}" echo "Analysis run: ${analysis_run_id}" echo "Detections: ${detection_count}"