fix(results): gate operational AI exports on QA
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 11:31:51 +02:00
parent b76cd1837b
commit 0209167cfd
10 changed files with 313 additions and 8 deletions
@@ -4,11 +4,12 @@ import json
from datetime import datetime, timezone
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from app.core.errors import AppError
from app.main import app
from app.models import Area, Dataset, Export, Project, QualityCheck, SourceRegistry, SourceSnapshot
from app.models import AnalysisRun, Area, Dataset, Export, Project, QualityCheck, SourceRegistry, SourceSnapshot
from app.schemas.export import ExportCreateResponse
from app.services.export_service import ExportService
from app.services.storage_service import StorageService
@@ -110,6 +111,143 @@ def _govern_fixture_dataset(dataset: Dataset) -> Dataset:
return dataset
def _authoritative_building_reference(dataset: Dataset) -> Dataset:
dataset = _govern_fixture_dataset(dataset)
dataset.dataset_role = "reference"
dataset.source_registry.usage_policy_json = {
"ground_truth_allowed": True,
"validation_authority": {"building_validation": "primary"},
}
return dataset
def test_detection_export_is_machine_labelled_as_unverified_review_output(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
run_id = uuid4()
source_dataset = _govern_fixture_dataset(
Dataset(
id=dataset_id,
project_id=project_id,
name="ortho.tif",
dataset_type="raster",
source="fixture",
status="ready",
)
)
run = AnalysisRun(
id=run_id,
project_id=project_id,
dataset_id=dataset_id,
analysis_type="detection",
status="success",
model_name="yolo-configured",
)
export_path = tmp_path / "detections-review.geojson"
db = FakeSession({(Dataset, dataset_id): source_dataset, (AnalysisRun, run_id): run})
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
response = ExportService.export_detection_run_geojson(db, run_id, intended_use="review")
content = json.loads(export_path.read_text(encoding="utf-8"))
trust = content["geointel_result"]
assert response.metadata_json["intended_use"] == "review"
assert trust["classification"] == "unverified_ai_review_output"
assert trust["authoritative"] is False
assert trust["operational_use_allowed"] is False
assert trust["blocking_reasons"] == ["authoritative_qa_missing"]
def test_detection_operational_export_fails_closed_without_authoritative_qa(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
run_id = uuid4()
source_dataset = _govern_fixture_dataset(
Dataset(id=dataset_id, project_id=project_id, name="ortho.tif", dataset_type="raster", source="fixture", status="ready")
)
run = AnalysisRun(
id=run_id,
project_id=project_id,
dataset_id=dataset_id,
analysis_type="detection",
status="success",
model_name="yolo-configured",
)
db = FakeSession({(Dataset, dataset_id): source_dataset, (AnalysisRun, run_id): run})
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(tmp_path / "blocked.geojson"))
with pytest.raises(AppError) as exc_info:
ExportService.export_detection_run_geojson(db, run_id, intended_use="operational")
assert exc_info.value.code == "DETECTION_OPERATIONAL_EXPORT_BLOCKED"
def test_detection_operational_export_requires_zero_error_authoritative_qa(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
reference_id = uuid4()
run_id = uuid4()
check_id = uuid4()
source_dataset = _govern_fixture_dataset(
Dataset(id=dataset_id, project_id=project_id, name="ortho.tif", dataset_type="raster", source="fixture", status="ready")
)
reference = _authoritative_building_reference(
Dataset(
id=reference_id,
project_id=project_id,
name="grb.geojson",
dataset_type="vector",
source="fixture",
dataset_role="reference",
status="ready",
)
)
run = AnalysisRun(
id=run_id,
project_id=project_id,
dataset_id=dataset_id,
analysis_type="detection",
status="success",
model_name="yolo-configured",
)
check = QualityCheck(
id=check_id,
project_id=project_id,
analysis_run_id=run_id,
candidate_dataset_id=dataset_id,
reference_dataset_id=reference_id,
check_type="detections_vs_reference",
status="ok",
findings_json={
"false_positives": 0,
"false_negatives": 0,
"warnings": [],
"unsupported_geometry": False,
"coverage": {"applied": True},
"temporal_compatibility": {"status": "compatible"},
},
created_at=datetime.now(timezone.utc),
)
export_path = tmp_path / "detections-operational.geojson"
db = FakeSession(
{
(Dataset, dataset_id): source_dataset,
(Dataset, reference_id): reference,
(AnalysisRun, run_id): run,
(QualityCheck, check_id): check,
}
)
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
response = ExportService.export_detection_run_geojson(db, run_id, intended_use="operational")
trust = json.loads(export_path.read_text(encoding="utf-8"))["geointel_result"]
assert response.metadata_json["intended_use"] == "operational"
assert trust["operational_use_allowed"] is True
assert trust["quality_check_id"] == str(check_id)
assert trust["reference_dataset_id"] == str(reference_id)
def test_dataset_geojson_export_persists_export_and_writes_artifact(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()