fix(results): gate operational AI exports on QA
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user