fix(ai): bind model scope to immutable geometry
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-08-09 10:54:52 +02:00
parent f41392a415
commit b76cd1837b
19 changed files with 431 additions and 58 deletions
+57 -4
View File
@@ -8,6 +8,8 @@ from types import SimpleNamespace
from uuid import uuid4
import pytest
from geoalchemy2.shape import from_shape
from shapely.geometry import box, mapping
from app.core.config import Settings
from app.core.errors import AppError
@@ -15,6 +17,7 @@ from app.models import AnalysisRun, Area, Dataset, Detection, Job, Project, Sour
from app.services.detection_georeferencing import pixel_bbox_to_epsg4326_polygon
from app.services.detection_service import DetectionService
from app.services.model_registry_service import ModelRegistryService
from app.services.model_validation_scope_service import ModelValidationScopeService
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
from app.services.yolo_adapter import YoloDetectionAdapter
@@ -207,6 +210,28 @@ def _settings(tmp_path: Path, **overrides) -> Settings:
return Settings(**values)
def _scope_settings(tmp_path: Path, scope_geometry=None, **overrides) -> Settings:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"scope-bound-model")
payload = {
"schema_version": ModelValidationScopeService.SCHEMA_VERSION,
"model_id": "yolo-configured",
"model_sha256": sha256(model_path.read_bytes()).hexdigest(),
"scope_key": "mol-kempen-test",
"crs": "EPSG:4326",
"geometry": mapping(scope_geometry or box(4.0, 50.8, 5.5, 52.0)),
}
manifest_path = tmp_path / "model-validation-scope.json"
manifest_path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
values = {
"yolo_model_path": str(model_path),
"yolo_validation_scope_manifest_path": str(manifest_path),
"yolo_validation_scope_manifest_sha256": sha256(manifest_path.read_bytes()).hexdigest(),
}
values.update(overrides)
return _settings(tmp_path, **values)
def _write_model_sidecar(
model_path: Path,
settings: Settings,
@@ -399,21 +424,49 @@ def test_yolo_runtime_rejects_cpu_device_when_cuda_is_required(tmp_path: Path, m
def test_yolo_validation_scope_requires_persisted_validated_area(tmp_path: Path) -> None:
dataset = Dataset(id=uuid4(), project_id=uuid4(), name="image.tif", dataset_type="raster", source="test", area_id=uuid4())
wrong_area = Area(id=dataset.area_id, project_id=dataset.project_id, name="Brussels", geometry="MULTIPOLYGON EMPTY")
wrong_area = Area(
id=dataset.area_id,
project_id=dataset.project_id,
name="Mol validation bypass",
geometry=from_shape(box(-74.1, 40.6, -73.8, 40.9), srid=4326),
)
db = FakeSession(objects={(Area, dataset.area_id): wrong_area})
with pytest.raises(AppError) as exc_info:
DetectionService._validate_model_area_scope(db, dataset, _settings(tmp_path, yolo_validated_area_names="Mol,Kempen"))
DetectionService._validate_model_area_scope(db, dataset, _scope_settings(tmp_path))
assert exc_info.value.code == "DETECTION_VALIDATION_SCOPE_UNAVAILABLE"
def test_yolo_validation_scope_accepts_bound_mol_area(tmp_path: Path) -> None:
dataset = Dataset(id=uuid4(), project_id=uuid4(), name="image.tif", dataset_type="raster", source="test", area_id=uuid4())
area = Area(id=dataset.area_id, project_id=dataset.project_id, name="Gemeente Mol", geometry="MULTIPOLYGON EMPTY")
area = Area(
id=dataset.area_id,
project_id=dataset.project_id,
name="Een wijzigbare weergavenaam",
geometry=from_shape(box(5.0, 51.1, 5.2, 51.3), srid=4326),
)
db = FakeSession(objects={(Area, dataset.area_id): area})
DetectionService._validate_model_area_scope(db, dataset, _settings(tmp_path, yolo_validated_area_names="Mol,Kempen"))
DetectionService._validate_model_area_scope(db, dataset, _scope_settings(tmp_path))
def test_yolo_validation_scope_rejects_tampered_manifest(tmp_path: Path) -> None:
dataset = Dataset(id=uuid4(), project_id=uuid4(), name="image.tif", dataset_type="raster", source="test", area_id=uuid4())
area = Area(
id=dataset.area_id,
project_id=dataset.project_id,
name="Gemeente Mol",
geometry=from_shape(box(5.0, 51.1, 5.2, 51.3), srid=4326),
)
settings = _scope_settings(tmp_path)
Path(settings.yolo_validation_scope_manifest_path).write_text("{}", encoding="utf-8")
db = FakeSession(objects={(Area, dataset.area_id): area})
with pytest.raises(AppError) as exc_info:
DetectionService._validate_model_area_scope(db, dataset, settings)
assert exc_info.value.code == "DETECTION_VALIDATION_SCOPE_CHECKSUM_MISMATCH"
def test_yolo_run_requires_tile_manifest_path(tmp_path: Path) -> None: