from __future__ import annotations import importlib.util import json import sys from datetime import datetime, timezone from pathlib import Path from types import SimpleNamespace import uuid import pytest from app.core.errors import AppError from app.models import Dataset from app.services.grb_refresh_plan_service import GrbRefreshPlanService from tests.frontend_contract import read_map_workspace NOW = datetime(2026, 7, 16, 16, 0, tzinfo=timezone.utc) PROJECT_ID = uuid.uuid4() class _Query: def __init__(self, rows: list[Dataset]) -> None: self.rows = rows def filter(self, *_args): return self def all(self) -> list[Dataset]: return self.rows class _Db: def __init__(self, rows: list[Dataset]) -> None: self.rows = rows def get(self, _model, identifier): return SimpleNamespace(id=identifier) def query(self, _model): return _Query(self.rows) def _dataset(theme: str, version: str = "2026-07-14", count: int = 100) -> Dataset: return Dataset( id=uuid.uuid4(), project_id=PROJECT_ID, name=f"grb_{theme}.geojson", dataset_type="vector", source="operator_official_import", source_name="grb", dataset_role="reference", reference_layer_name=theme, source_version=version, temporal_series_key=f"grb:{theme}:kempen-transport-region", observed_at=datetime.fromisoformat(f"{version}T00:00:00+00:00"), imported_at=NOW, metadata_json={"feature_count": count}, size_bytes=1000, status="ready", ) def _catalog(status: str = "available", version: str | None = "Toestand 2026-07-15"): return SimpleNamespace( items=[ SimpleNamespace( source_name="grb", status=status, reachable=status == "available", remote_version=version, checked_at=NOW, ) ] ) def test_refresh_plan_marks_all_older_immutable_snapshots_as_update_available(monkeypatch) -> None: rows = [_dataset(theme, count=(index + 1) * 100) for index, theme in enumerate(("buildings", "roads", "water", "parcels"))] monkeypatch.setattr( "app.services.grb_refresh_plan_service.SourceCatalogProbeService.audit_project", lambda *_args, **_kwargs: _catalog(), ) plan = GrbRefreshPlanService.build(_Db(rows), PROJECT_ID, now=NOW) assert plan.remote_edition_date.isoformat() == "2026-07-15" assert plan.summary.update_available_count == 4 assert plan.summary.new_dataset_count_if_applied == 4 assert plan.summary.retained_dataset_count == 4 assert plan.summary.current_feature_count == 1000 assert plan.summary.current_size_bytes == 4000 assert all(item.status == "update_available" for item in plan.layers) assert all(item.retained_after_refresh for item in plan.layers) assert plan.automatic_import is False assert plan.destructive_replacement is False @pytest.mark.parametrize( ("rows", "catalog", "expected"), [ ([_dataset("buildings", "2026-07-15")], _catalog(), "current"), ([], _catalog(), "not_loaded"), ([_dataset("buildings")], _catalog("unavailable"), "remote_unavailable"), ([_dataset("buildings")], _catalog("available", "Onbekende toestand"), "review_required"), ], ) def test_refresh_plan_status_matrix(monkeypatch, rows, catalog, expected) -> None: monkeypatch.setattr( "app.services.grb_refresh_plan_service.SourceCatalogProbeService.audit_project", lambda *_args, **_kwargs: catalog, ) plan = GrbRefreshPlanService.build(_Db(rows), PROJECT_ID, now=NOW) buildings = next(item for item in plan.layers if item.theme == "buildings") assert buildings.status == expected def test_refresh_plan_rejects_an_unapproved_scope(monkeypatch) -> None: monkeypatch.setattr( "app.services.grb_refresh_plan_service.SourceCatalogProbeService.audit_project", lambda *_args, **_kwargs: _catalog(), ) with pytest.raises(AppError) as raised: GrbRefreshPlanService.build(_Db([]), PROJECT_ID, scope="mol", now=NOW) assert raised.value.code == "GRB_REFRESH_SCOPE_UNSUPPORTED" def test_refresh_plan_route_uses_the_canonical_envelope(monkeypatch) -> None: from app.api.routes import datasets as dataset_routes monkeypatch.setattr( "app.services.grb_refresh_plan_service.SourceCatalogProbeService.audit_project", lambda *_args, **_kwargs: _catalog(), ) response = dataset_routes.plan_grb_dataset_refresh( project_id=PROJECT_ID, scope="kempen-transport-region", refresh_catalog=False, db=_Db([]), ) assert list(response) == ["data"] assert response["data"]["project_id"] == PROJECT_ID assert response["data"]["automatic_import"] is False def _load_operator_module(): root = Path(__file__).resolve().parents[2] scripts = root / "scripts" if str(scripts) not in sys.path: sys.path.insert(0, str(scripts)) spec = importlib.util.spec_from_file_location("manage_grb_refresh_s223", scripts / "manage_grb_refresh.py") assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def test_operator_requires_exact_edition_and_plan_hash() -> None: module = _load_operator_module() assert module.require_confirmed_edition({"remote_edition_date": "2026-07-15"}, "2026-07-15") == "2026-07-15" with pytest.raises(RuntimeError, match="confirm-edition"): module.require_confirmed_edition({"remote_edition_date": "2026-07-15"}, "2026-07-14") payload = {"status": "staged", "layers": []} first = module.canonical_plan_sha256(payload) assert first == module.canonical_plan_sha256({**payload, "plan_sha256": first}) assert first != module.canonical_plan_sha256({"status": "staged", "layers": [{"theme": "roads"}]}) def test_operator_validates_every_staged_artifact_and_partition(tmp_path) -> None: module = _load_operator_module() manifest_dir = tmp_path / "kempen-transport-region" / "buildings" / "2026-07-15" partition_dir = manifest_dir / "partitions" partition_dir.mkdir(parents=True) artifact = manifest_dir / "buildings.geojson" partition = partition_dir / "13025_mol.geojson" artifact.write_text('{"type":"FeatureCollection","features":[]}', encoding="utf-8") partition.write_text('{"type":"FeatureCollection","features":[]}', encoding="utf-8") manifest = { "status": "complete", "scope": "kempen-transport-region", "theme": "buildings", "observed_at": "2026-07-15", "reference_truncated": False, "member_count": 1, "feature_count": 1, "artifact_filename": artifact.name, "artifact_sha256": module.sha256_file(artifact), "artifact_size_bytes": artifact.stat().st_size, "partitions": [{"filename": partition.name, "sha256": module.sha256_file(partition)}], } path = manifest_dir / "regional_buildings_manifest.json" path.write_text(json.dumps(manifest), encoding="utf-8") assert module.validate_manifest( path, output_root=tmp_path, scope="kempen-transport-region", theme="buildings", edition="2026-07-15", )["feature_count"] == 1 partition.write_text("changed", encoding="utf-8") with pytest.raises(RuntimeError, match="partition checksum"): module.validate_manifest( path, output_root=tmp_path, scope="kempen-transport-region", theme="buildings", edition="2026-07-15", ) def test_operator_builds_only_allowlisted_local_subprocess_commands(tmp_path) -> None: module = _load_operator_module() args = SimpleNamespace( scope="kempen-transport-region", api_url="http://127.0.0.1:8000/api/v1", output_root=tmp_path, request_timeout=180, api_timeout=180, batch_size=1000, page_limit=1000, max_features_per_member=100000, max_total_features=1500000, ) commands = module.build_operator_commands( args, ["buildings", "roads", "water", "parcels"], "2026-07-15", fetch_only=True, ) flattened = [argument for _label, command in commands for argument in command] assert len(commands) == 2 assert "--fetch-only" in flattened assert "provision_regional_grb_buildings.py" in " ".join(flattened) assert "provision_regional_grb_context.py" in " ".join(flattened) assert "--force" not in flattened def test_refresh_api_and_frontend_remain_explicit_only() -> None: root = Path(__file__).resolve().parents[2] service = (root / "backend" / "app" / "services" / "grb_refresh_plan_service.py").read_text(encoding="utf-8") hook = (root / "frontend" / "src" / "hooks" / "useSourceFreshness.ts").read_text(encoding="utf-8") operator = (root / "scripts" / "manage_grb_refresh.py").read_text(encoding="utf-8") assert "DatasetService" not in service assert "VectorFeatureService" not in service assert "void probeCatalogs(" not in hook assert 'choices=("plan", "stage", "apply")' in operator assert "--confirm-plan-sha256" in operator def test_map_theme_ranking_prefers_newer_observation_over_feature_count() -> None: workspace = read_map_workspace() observed_sort = workspace.index("const observedAtDifference") feature_tiebreaker = workspace.index("right.feature_count", observed_sort) assert observed_sort < feature_tiebreaker assert "new Date(right.observed_at ?? 0).getTime()" in workspace assert "if (observedAtDifference !== 0) return observedAtDifference" in workspace def test_map_workspace_restores_theme_from_selected_dataset() -> None: workspace = read_map_workspace() assert "function themeIdForDataset(" in workspace assert "useState(() =>" in workspace assert "return themeIdForDataset(selectedDataset) ?? 'buildings'" in workspace assert "const matchingThemeId = themeIdForDataset(selectedMapDataset)" in workspace