GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
450 lines
18 KiB
Python
450 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from hashlib import sha256
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from shapely.geometry import box
|
|
|
|
from app.core.errors import AppError
|
|
from app.services.data_contract_validation import (
|
|
AttributeRule,
|
|
BoundingBox,
|
|
ContractKind,
|
|
DataAssetValidationInput,
|
|
DataContract,
|
|
DataContractRegistry,
|
|
DataContractValidator,
|
|
FreshnessRules,
|
|
GeometryRecord,
|
|
GeometryRules,
|
|
LineageEvidence,
|
|
LineageRules,
|
|
RasterRules,
|
|
RequirementLevel,
|
|
Resolution,
|
|
ResolutionRules,
|
|
TransformationEvidence,
|
|
ValidationStatus,
|
|
build_default_data_contract_registry,
|
|
build_label_validation_input,
|
|
build_model_validation_input,
|
|
build_raster_ingest_input,
|
|
build_vector_ingest_input,
|
|
validate_registered_asset,
|
|
)
|
|
from app.services.data_quarantine_service import AssetUse, DataQuarantineService
|
|
|
|
|
|
FIXTURE_ROOT = Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "data-contracts"
|
|
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc)
|
|
CHECKSUM_A = "a" * 64
|
|
|
|
|
|
def _fixture_json(name: str) -> tuple[bytes, object]:
|
|
raw = (FIXTURE_ROOT / name).read_bytes()
|
|
return raw, json.loads(raw)
|
|
|
|
|
|
def _checksum(raw: bytes) -> str:
|
|
return sha256(raw).hexdigest()
|
|
|
|
|
|
def _lineage_with_transform() -> LineageEvidence:
|
|
return LineageEvidence(
|
|
transformations=(
|
|
TransformationEvidence(
|
|
name="epsg31370-to-epsg4326",
|
|
version="1.0.0",
|
|
checksum_sha256=CHECKSUM_A,
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def _vector_input_from_fixture(name: str, *, source_crs: str = "EPSG:31370", storage_crs: str = "EPSG:4326") -> DataAssetValidationInput:
|
|
raw, payload = _fixture_json(name)
|
|
assert isinstance(payload, dict)
|
|
return build_vector_ingest_input(
|
|
asset_id=f"fixture:{name}",
|
|
source_crs=source_crs,
|
|
storage_crs=storage_crs,
|
|
feature_collection=payload,
|
|
checksum_sha256=_checksum(raw),
|
|
computed_checksum_sha256=_checksum(raw),
|
|
content=raw,
|
|
source_registry_id="source:digitaal-vlaanderen:grb",
|
|
source_snapshot_id="snapshot:grb:2026-07-31",
|
|
imported_at=NOW,
|
|
metadata={"license": "Open Data Licence", "provider": "Digitaal Vlaanderen"},
|
|
observed_at=NOW - timedelta(days=1),
|
|
source_version="2026.07.31",
|
|
lineage=_lineage_with_transform() if source_crs != storage_crs else LineageEvidence(),
|
|
)
|
|
|
|
|
|
def _issue_codes(report) -> set[str]:
|
|
return {issue.code for issue in report.issues}
|
|
|
|
|
|
def test_default_vector_contract_accepts_transformed_geojson_with_complete_provenance() -> None:
|
|
report = validate_registered_asset(_vector_input_from_fixture("vector-building-valid.geojson"), now=NOW)
|
|
|
|
assert report.validation_status == ValidationStatus.PASSED
|
|
assert report.quarantine_status == "not_quarantined"
|
|
assert report.provenance_status == "complete"
|
|
assert report.lineage_status == "complete"
|
|
persisted = report.persistence_fields()
|
|
assert persisted["data_contract_key"] == "geointel.vector.geojson"
|
|
assert persisted["data_contract_version"] == "1.0.0"
|
|
assert persisted["validation_report_json"]["report_sha256"] == report.report_sha256
|
|
|
|
|
|
def test_default_vector_contract_quarantines_lambert_coordinates_mislabelled_as_epsg4326() -> None:
|
|
report = validate_registered_asset(
|
|
_vector_input_from_fixture(
|
|
"vector-lambert-mislabelled-as-4326.geojson",
|
|
source_crs="EPSG:4326",
|
|
storage_crs="EPSG:4326",
|
|
),
|
|
now=NOW,
|
|
)
|
|
|
|
assert report.validation_status == ValidationStatus.FAILED
|
|
assert report.quarantine_status == "quarantined"
|
|
assert "CRS_COORDINATE_DOMAIN_VIOLATION" in _issue_codes(report)
|
|
|
|
|
|
def test_vector_contract_checks_geometry_attributes_bounds_and_topology_fail_closed() -> None:
|
|
contract = DataContract(
|
|
key="test.vector.buildings",
|
|
version="1.0.0",
|
|
kind=ContractKind.VECTOR,
|
|
accepted_source_crs=frozenset({"EPSG:4326"}),
|
|
canonical_storage_crs="EPSG:4326",
|
|
spatial_domain=BoundingBox(2.0, 49.0, 7.0, 52.0),
|
|
require_bounds=True,
|
|
geometry_rules=GeometryRules(
|
|
allowed_geometry_types=frozenset({"Polygon"}),
|
|
attribute_rules=(AttributeRule("native_id", accepted_types=("integer",)),),
|
|
forbid_shared_area=True,
|
|
),
|
|
)
|
|
raw = b"overlapping-vector"
|
|
asset = DataAssetValidationInput(
|
|
asset_id="vector:bad",
|
|
data_contract_key=contract.key,
|
|
data_contract_version=contract.version,
|
|
kind=ContractKind.VECTOR,
|
|
source_crs="EPSG:4326",
|
|
storage_crs="EPSG:4326",
|
|
bounds=BoundingBox(4.0, 51.0, 4.1, 51.1),
|
|
checksum_sha256=_checksum(raw),
|
|
computed_checksum_sha256=_checksum(raw),
|
|
content=raw,
|
|
geometry_records=(
|
|
GeometryRecord(box(4.0, 51.0, 4.05, 51.05), {"native_id": "wrong-type"}),
|
|
GeometryRecord(box(4.025, 51.025, 4.075, 51.075), {}),
|
|
),
|
|
source_registry_id="source:test",
|
|
source_snapshot_id="snapshot:test",
|
|
imported_at=NOW,
|
|
)
|
|
|
|
report = DataContractValidator.validate(contract, asset, now=NOW)
|
|
|
|
assert report.validation_status == ValidationStatus.FAILED
|
|
assert {"ATTRIBUTE_TYPE_INVALID", "ATTRIBUTE_REQUIRED", "TOPOLOGY_SHARED_AREA"} <= _issue_codes(report)
|
|
assert "BOUNDS_GEOMETRY_MISMATCH" in _issue_codes(report)
|
|
|
|
|
|
def test_default_vector_contract_validates_replayable_large_partition_stream_without_materialising_geometry_list() -> None:
|
|
"""Regional imports may be large but remain fully schema/domain checked.
|
|
|
|
The default contract has no source-specific shared-area rule, so the
|
|
validator must make its bounds/schema passes over a replayable stream
|
|
without accumulating every Shapely geometry in memory. A stricter
|
|
source-specific contract can still opt into a bounded topology batch.
|
|
"""
|
|
|
|
class ReplayableRecords:
|
|
def __init__(self, count: int) -> None:
|
|
self.count = count
|
|
self.iterations = 0
|
|
|
|
def __iter__(self):
|
|
self.iterations += 1
|
|
for index in range(self.count):
|
|
yield GeometryRecord(
|
|
box(4.69, 51.09, 4.70, 51.10),
|
|
{"partition_feature": index},
|
|
)
|
|
|
|
raw = b"partitioned-vector-stream"
|
|
records = ReplayableRecords(12_000)
|
|
asset = DataAssetValidationInput(
|
|
asset_id="vector:partitioned-stream",
|
|
data_contract_key="geointel.vector.geojson",
|
|
data_contract_version="1.0.0",
|
|
kind=ContractKind.VECTOR,
|
|
source_crs="EPSG:4326",
|
|
storage_crs="EPSG:4326",
|
|
bounds=BoundingBox(4.69, 51.09, 4.70, 51.10),
|
|
checksum_sha256=_checksum(raw),
|
|
computed_checksum_sha256=_checksum(raw),
|
|
content=raw,
|
|
metadata={"license": "Open Data"},
|
|
geometry_records=records,
|
|
source_registry_id="source:grb",
|
|
source_snapshot_id="snapshot:grb:partitioned",
|
|
imported_at=NOW,
|
|
observed_at=NOW,
|
|
source_version="2026-08-01",
|
|
)
|
|
|
|
report = validate_registered_asset(asset, now=NOW)
|
|
|
|
assert report.validation_status == ValidationStatus.PASSED
|
|
assert records.iterations >= 2
|
|
|
|
|
|
def test_raster_contract_accepts_explicit_units_and_quarantines_stale_bad_profile() -> None:
|
|
raw = b"raster-stage"
|
|
valid = build_raster_ingest_input(
|
|
asset_id="raster:valid",
|
|
source_crs="EPSG:31370",
|
|
storage_crs="EPSG:31370",
|
|
raster_profile={"width": 512, "height": 512, "band_count": 3, "dtype": ["uint8"]},
|
|
bounds=BoundingBox(193_277.5, 205_708.3, 193_777.5, 206_208.3),
|
|
resolution=Resolution(0.9765625, 0.9765625, "m"),
|
|
checksum_sha256=_checksum(raw),
|
|
computed_checksum_sha256=_checksum(raw),
|
|
content=raw,
|
|
source_registry_id="source:orthophoto",
|
|
source_snapshot_id="snapshot:orthophoto:2026.01",
|
|
imported_at=NOW,
|
|
metadata={"license": "Open Data"},
|
|
observed_at=None,
|
|
temporal_unknown_reason="latest mosaic has no per-pixel observation date",
|
|
source_version=None,
|
|
source_version_unknown_reason="provider did not publish an edition",
|
|
)
|
|
assert validate_registered_asset(valid, now=NOW).validation_status == ValidationStatus.PASSED
|
|
|
|
strict = DataContract(
|
|
key="test.raster.strict",
|
|
version="1.0.0",
|
|
kind=ContractKind.RASTER,
|
|
accepted_source_crs=frozenset({"EPSG:31370"}),
|
|
require_bounds=True,
|
|
raster_rules=RasterRules(allowed_band_counts=frozenset({3}), allowed_dtypes=frozenset({"uint8"})),
|
|
resolution_rules=ResolutionRules(allowed_units=frozenset({"m"}), min_x=0.2, max_x=1.0, min_y=0.2, max_y=1.0),
|
|
freshness_rules=FreshnessRules(observed_at=RequirementLevel.REQUIRED, max_age=timedelta(days=30)),
|
|
lineage_rules=LineageRules(require_transformation_when_crs_changes=False),
|
|
)
|
|
invalid = DataAssetValidationInput(
|
|
asset_id="raster:bad",
|
|
data_contract_key=strict.key,
|
|
data_contract_version=strict.version,
|
|
kind=ContractKind.RASTER,
|
|
source_crs="EPSG:31370",
|
|
storage_crs="EPSG:31370",
|
|
bounds=BoundingBox(100.0, 100.0, 200.0, 200.0),
|
|
checksum_sha256=_checksum(raw),
|
|
computed_checksum_sha256=_checksum(raw),
|
|
content=raw,
|
|
raster_profile={"width": 0, "height": 10, "band_count": 2, "dtype": ["float32"]},
|
|
resolution=Resolution(2.0, 0.1, "degree"),
|
|
source_registry_id="source:raster",
|
|
source_snapshot_id="snapshot:raster",
|
|
imported_at=NOW,
|
|
observed_at=NOW - timedelta(days=31),
|
|
)
|
|
report = DataContractValidator.validate(strict, invalid, now=NOW)
|
|
|
|
assert report.validation_status == ValidationStatus.FAILED
|
|
assert {
|
|
"RASTER_PROFILE_VALUE_INVALID",
|
|
"RASTER_BAND_COUNT_NOT_ALLOWED",
|
|
"RASTER_DTYPE_NOT_ALLOWED",
|
|
"RESOLUTION_UNIT_NOT_ALLOWED",
|
|
"RESOLUTION_OUT_OF_RANGE",
|
|
"FRESHNESS_EXCEEDED",
|
|
} <= _issue_codes(report)
|
|
|
|
|
|
def test_default_label_and_model_contracts_validate_good_and_bad_fixtures() -> None:
|
|
valid_raw, valid_labels = _fixture_json("labels-valid.json")
|
|
invalid_raw, invalid_labels = _fixture_json("labels-invalid.json")
|
|
assert isinstance(valid_labels, list)
|
|
assert isinstance(invalid_labels, list)
|
|
lineage = LineageEvidence(upstream_asset_ids=("image:1",), upstream_checksums_sha256=(CHECKSUM_A,))
|
|
valid_label = build_label_validation_input(
|
|
asset_id="label:valid",
|
|
label_records=valid_labels,
|
|
checksum_sha256=_checksum(valid_raw),
|
|
computed_checksum_sha256=_checksum(valid_raw),
|
|
content=valid_raw,
|
|
source_registry_id="source:labels",
|
|
source_snapshot_id="snapshot:labels:1",
|
|
imported_at=NOW,
|
|
metadata={
|
|
"image_checksum_sha256": CHECKSUM_A,
|
|
"class_ontology_version": "buildings-v1",
|
|
"source_corpus_manifest_sha256": CHECKSUM_A,
|
|
},
|
|
temporal_unknown_reason="labels inherit image observation handling",
|
|
source_version_unknown_reason="label release is represented by its snapshot",
|
|
lineage=lineage,
|
|
)
|
|
valid_report = validate_registered_asset(valid_label, now=NOW)
|
|
assert valid_report.validation_status == ValidationStatus.PASSED
|
|
|
|
invalid_label = build_label_validation_input(
|
|
asset_id="label:invalid",
|
|
label_records=invalid_labels,
|
|
checksum_sha256=_checksum(invalid_raw),
|
|
computed_checksum_sha256=_checksum(invalid_raw),
|
|
content=invalid_raw,
|
|
source_registry_id="source:labels",
|
|
source_snapshot_id="snapshot:labels:1",
|
|
imported_at=NOW,
|
|
metadata={
|
|
"image_checksum_sha256": "not-a-sha256",
|
|
"class_ontology_version": "buildings-v1",
|
|
"source_corpus_manifest_sha256": CHECKSUM_A,
|
|
},
|
|
temporal_unknown_reason="labels inherit image observation handling",
|
|
source_version_unknown_reason="label release is represented by its snapshot",
|
|
lineage=lineage,
|
|
)
|
|
invalid_report = validate_registered_asset(invalid_label, now=NOW)
|
|
assert invalid_report.validation_status == ValidationStatus.FAILED
|
|
assert {
|
|
"LABEL_CLASS_ID_NOT_ALLOWED",
|
|
"LABEL_NORMALIZED_COORDINATE_INVALID",
|
|
"METADATA_CHECKSUM_INVALID",
|
|
} <= _issue_codes(invalid_report)
|
|
|
|
pure_background_raw = b""
|
|
pure_background_metadata = {
|
|
"image_checksum_sha256": CHECKSUM_A,
|
|
"class_ontology_version": "buildings-v1",
|
|
"source_corpus_manifest_sha256": CHECKSUM_A,
|
|
"label_mode": "pure_background",
|
|
"sample_slug": "forest-background-aoi",
|
|
"split": "train",
|
|
"raster_dataset_id": "dataset:raster:1",
|
|
"reference_dataset_id": "dataset:reference:1",
|
|
"review_decision": "accepted",
|
|
"reviewer_id": "reviewer@example.test",
|
|
"reviewed_at": "2026-08-01T11:00:00+00:00",
|
|
"review_artifact_sha256": CHECKSUM_A,
|
|
}
|
|
pure_background = build_label_validation_input(
|
|
asset_id="label:pure-background",
|
|
label_records=(),
|
|
label_mode="pure_background",
|
|
checksum_sha256=_checksum(pure_background_raw),
|
|
computed_checksum_sha256=_checksum(pure_background_raw),
|
|
content=pure_background_raw,
|
|
source_registry_id="source:labels",
|
|
source_snapshot_id="snapshot:labels:1",
|
|
imported_at=NOW,
|
|
metadata=pure_background_metadata,
|
|
temporal_unknown_reason="labels inherit image observation handling",
|
|
source_version_unknown_reason="label release is represented by its snapshot",
|
|
lineage=LineageEvidence(
|
|
upstream_asset_ids=("dataset:raster:1", "dataset:reference:1"),
|
|
upstream_checksums_sha256=(CHECKSUM_A, CHECKSUM_A),
|
|
),
|
|
)
|
|
assert validate_registered_asset(pure_background, now=NOW).validation_status == ValidationStatus.PASSED
|
|
|
|
unmarked_empty = build_label_validation_input(
|
|
asset_id="label:unmarked-empty",
|
|
label_records=(),
|
|
checksum_sha256=_checksum(pure_background_raw),
|
|
computed_checksum_sha256=_checksum(pure_background_raw),
|
|
content=pure_background_raw,
|
|
source_registry_id="source:labels",
|
|
source_snapshot_id="snapshot:labels:1",
|
|
imported_at=NOW,
|
|
metadata={
|
|
"image_checksum_sha256": CHECKSUM_A,
|
|
"class_ontology_version": "buildings-v1",
|
|
"source_corpus_manifest_sha256": CHECKSUM_A,
|
|
},
|
|
temporal_unknown_reason="labels inherit image observation handling",
|
|
source_version_unknown_reason="label release is represented by its snapshot",
|
|
lineage=LineageEvidence(
|
|
upstream_asset_ids=("dataset:raster:1", "dataset:reference:1"),
|
|
upstream_checksums_sha256=(CHECKSUM_A, CHECKSUM_A),
|
|
),
|
|
)
|
|
assert "PURE_BACKGROUND_MODE_REQUIRED" in _issue_codes(validate_registered_asset(unmarked_empty, now=NOW))
|
|
|
|
model_raw = b"model-asset"
|
|
model = build_model_validation_input(
|
|
asset_id="model:valid",
|
|
model_metadata={"model_format": "pytorch", "framework": "torch", "class_mapping": {"0": "building"}},
|
|
checksum_sha256=_checksum(model_raw),
|
|
computed_checksum_sha256=_checksum(model_raw),
|
|
content=model_raw,
|
|
source_registry_id="source:model-registry",
|
|
source_snapshot_id="snapshot:model:1",
|
|
imported_at=NOW,
|
|
source_version="candidate-1",
|
|
metadata={"training_manifest_sha256": CHECKSUM_A, "runtime_manifest_sha256": CHECKSUM_A},
|
|
lineage=lineage,
|
|
)
|
|
assert validate_registered_asset(model, now=NOW).validation_status == ValidationStatus.PASSED
|
|
|
|
|
|
def test_unknown_contract_and_quarantine_gate_are_deterministic_and_fail_closed() -> None:
|
|
unknown = DataAssetValidationInput(
|
|
asset_id="asset:unknown",
|
|
data_contract_key="does.not.exist",
|
|
data_contract_version="9.9.9",
|
|
kind=ContractKind.VECTOR,
|
|
)
|
|
report = DataContractRegistry().validate(unknown, now=NOW)
|
|
assert report.validation_status == ValidationStatus.FAILED
|
|
assert report.quarantine_status == "quarantined"
|
|
assert _issue_codes(report) == {"DATA_CONTRACT_UNKNOWN"}
|
|
|
|
first = DataQuarantineService.decide(report)
|
|
second = DataQuarantineService.decide(report)
|
|
assert first.idempotency_key == second.idempotency_key
|
|
assert first.reason_codes == ("DATA_CONTRACT_UNKNOWN",)
|
|
with pytest.raises(AppError, match="cannot enter this pipeline") as exc_info:
|
|
DataQuarantineService.require_eligible(first, use=AssetUse.PRODUCTION_INFERENCE)
|
|
assert exc_info.value.code == "DATASET_QUARANTINED"
|
|
assert exc_info.value.details["use"] == "production_inference"
|
|
|
|
clean_report = validate_registered_asset(_vector_input_from_fixture("vector-building-valid.geojson"), now=NOW)
|
|
release_request = DataQuarantineService.decide(clean_report, previous=first)
|
|
assert release_request.quarantine_status == "quarantined"
|
|
assert release_request.requires_explicit_release is True
|
|
assert release_request.reason_codes == ("QUARANTINE_RELEASE_REQUIRES_EXPLICIT_PERSISTENCE",)
|
|
|
|
|
|
def test_registry_requires_exact_contract_version_and_fingerprints_schema() -> None:
|
|
registry = build_default_data_contract_registry()
|
|
version_mismatch = _vector_input_from_fixture("vector-building-valid.geojson")
|
|
mismatched = DataAssetValidationInput(
|
|
**{**version_mismatch.__dict__, "data_contract_version": "2.0.0"},
|
|
)
|
|
|
|
report = registry.validate(mismatched, now=NOW)
|
|
assert report.validation_status == ValidationStatus.FAILED
|
|
assert "DATA_CONTRACT_UNKNOWN" in _issue_codes(report)
|
|
|
|
contract = registry.resolve("geointel.vector.geojson", "1.0.0")
|
|
assert contract is not None
|
|
direct_report = DataContractValidator.validate(contract, mismatched, now=NOW)
|
|
assert direct_report.validation_status == ValidationStatus.FAILED
|
|
assert "DATA_CONTRACT_IDENTITY_MISMATCH" in _issue_codes(direct_report)
|