Add model asset detection workflow smoke
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-06 23:55:58 +02:00
parent 0ca3f93fbc
commit b2fe7fa8bb
9 changed files with 376 additions and 0 deletions
+16
View File
@@ -135,6 +135,22 @@ The model-load smoke is opt-in, requires real optional AI dependencies, refuses
`--assume-dependencies`, loads only the supplied local file and does not download
weights or run prediction.
Verify the full configured-YOLO model asset workflow against a running runtime:
```bash
bash scripts/verify_model_asset_detection_workflow.sh http://192.168.10.150:1202
```
This smoke is intentionally mutating and requires a real AI-enabled runtime with
at least one mounted local model asset. It seeds the explicit offline demo
workflow, generates a small raster tile manifest, selects the active local model
asset from `GET /api/v1/detection/model-assets`, validates read-only YOLO
preflight, runs `POST /api/v1/detection/run`, and verifies the persisted
AnalysisRun, Detection list and Detection GeoJSON endpoints. A zero detection
count is allowed because the demo raster is a synthetic runtime fixture; the
script validates the operational path and provenance, not production model
quality. The main readiness gate checks this script's syntax only.
Docker images install only the GIS runtime by default. To build a local/Tower
image with PyTorch/Ultralytics available for the configured-YOLO preflight and
runtime path, set:
+1
View File
@@ -53,6 +53,7 @@ bash -n scripts/verify_browser_runtime.sh
bash -n scripts/verify_demo_export_workflow.sh
bash -n scripts/verify_demo_raster_workflow.sh
bash -n scripts/verify_ai_handoff_interactions.sh
bash -n scripts/verify_model_asset_detection_workflow.sh
bash -n scripts/verify_workbench_default_state.sh
bash -n scripts/verify_workbench_interactions.sh
bash -n scripts/verify_gis_runtime.sh
@@ -0,0 +1,258 @@
#!/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}"