release: bump version to 0.49.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,45 @@
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from typing import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bgp_collector_location_cache():
|
||||
"""Mirror app startup seeding for tests that call sync BGP helpers."""
|
||||
from app.services.bgp_collector_locations import (
|
||||
SEED_PATH,
|
||||
set_bgp_collector_location_cache,
|
||||
)
|
||||
|
||||
payload = json.loads(SEED_PATH.read_text(encoding="utf-8"))
|
||||
cache = {}
|
||||
for entry in payload.get("locations", []):
|
||||
collector_id = next(
|
||||
alias for alias in entry.get("aliases", []) if str(alias).startswith("rrc")
|
||||
)
|
||||
cache[collector_id] = {
|
||||
"city": entry.get("city"),
|
||||
"country": entry.get("country"),
|
||||
"latitude": entry.get("latitude"),
|
||||
"longitude": entry.get("longitude"),
|
||||
"precision": entry.get("precision") or "city",
|
||||
"source": "legacy_seed",
|
||||
"needs_confirmation": True,
|
||||
"matched_location_name": entry.get("site") or collector_id,
|
||||
"verified_at": None,
|
||||
"confidence": entry.get("confidence"),
|
||||
"operator": entry.get("operator"),
|
||||
"site": entry.get("site"),
|
||||
"verification_status": "unverified",
|
||||
"source_note": entry.get("source_note"),
|
||||
}
|
||||
set_bgp_collector_location_cache(cache)
|
||||
yield
|
||||
set_bgp_collector_location_cache({})
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for BGP observability helpers."""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
@@ -54,11 +55,34 @@ class _FakeResult:
|
||||
def scalars(self):
|
||||
return _FakeScalarResult(self._rows)
|
||||
|
||||
def all(self):
|
||||
if self._rows and all(isinstance(row, BGPObservation) for row in self._rows):
|
||||
return [
|
||||
(row.prefix, row.origin_asn, row.collector, row.collector_geo)
|
||||
for row in self._rows
|
||||
]
|
||||
return self._rows
|
||||
|
||||
def scalar(self):
|
||||
if not self._rows:
|
||||
return 0
|
||||
first = self._rows[0]
|
||||
if isinstance(first, (int, float, str)):
|
||||
return first
|
||||
if isinstance(first, tuple) and len(first) == 1:
|
||||
return first[0]
|
||||
return len(self._rows)
|
||||
|
||||
def fetchall(self):
|
||||
return self._rows
|
||||
|
||||
def fetchone(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
if not self._rows:
|
||||
return None
|
||||
first = self._rows[0]
|
||||
if isinstance(first, CollectedData):
|
||||
return {"extra_data": first.extra_data}
|
||||
return first
|
||||
|
||||
|
||||
class _FakeAsyncSession:
|
||||
@@ -988,7 +1012,7 @@ async def test_infer_related_infrastructure_links_nearby_cables():
|
||||
data_type="cable",
|
||||
extra_data={"cable_id": 20},
|
||||
)
|
||||
db = _FakeAsyncSession([[landing], [relation], [cable]])
|
||||
db = _FakeAsyncSession([[landing, relation, cable]])
|
||||
|
||||
result = await infer_related_infrastructure(
|
||||
db,
|
||||
@@ -1012,27 +1036,37 @@ async def test_infer_related_infrastructure_links_nearby_cables():
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_bgp_collector_coverage_summarizes_observations():
|
||||
now = datetime.now(UTC)
|
||||
obs_one = BGPObservation(
|
||||
source="ris_live_bgp",
|
||||
aggregate = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
observation_count=2,
|
||||
prefix_count=2,
|
||||
origin_asn_count=2,
|
||||
peer_asn_count=2,
|
||||
recent_15m_observation_count=2,
|
||||
recent_24h_observation_count=2,
|
||||
recent_7d_observation_count=2,
|
||||
recent_15m_prefix_count=2,
|
||||
recent_24h_prefix_count=2,
|
||||
recent_7d_prefix_count=2,
|
||||
latest_observed_at=now + timedelta(minutes=5),
|
||||
)
|
||||
latest = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
latest_event_type="withdrawal",
|
||||
country="Netherlands",
|
||||
city="Amsterdam",
|
||||
)
|
||||
top_event = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
prefix="203.0.113.0/24",
|
||||
origin_asn=64496,
|
||||
peer_asn=3333,
|
||||
event_type="announcement",
|
||||
observed_at=now,
|
||||
collector_geo={"city": "Amsterdam", "country": "Netherlands"},
|
||||
count=1,
|
||||
)
|
||||
obs_two = BGPObservation(
|
||||
source="ris_live_bgp",
|
||||
scope = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
prefix="198.51.100.0/24",
|
||||
origin_asn=64497,
|
||||
peer_asn=3334,
|
||||
event_type="withdrawal",
|
||||
observed_at=now + timedelta(minutes=5),
|
||||
collector_geo={"city": "Amsterdam", "country": "Netherlands"},
|
||||
country="Netherlands",
|
||||
city="Amsterdam",
|
||||
)
|
||||
db = _FakeAsyncSession([[obs_one, obs_two]])
|
||||
db = _FakeAsyncSession([[aggregate], [latest], [top_event], [scope]])
|
||||
|
||||
coverage = await build_bgp_collector_coverage(db, source_filter=BGP_SOURCES)
|
||||
|
||||
@@ -1363,18 +1397,39 @@ async def test_bgp_event_summary_api_returns_aggregates():
|
||||
@pytest.mark.asyncio
|
||||
async def test_bgp_collectors_api_returns_coverage():
|
||||
now = datetime.now(UTC)
|
||||
observation = BGPObservation(
|
||||
id=1,
|
||||
source="ris_live_bgp",
|
||||
aggregate = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
peer_asn=3333,
|
||||
prefix="203.0.113.0/24",
|
||||
event_type="announcement",
|
||||
origin_asn=64496,
|
||||
observed_at=now,
|
||||
collector_geo={"city": "Amsterdam", "country": "Netherlands"},
|
||||
observation_count=1,
|
||||
prefix_count=1,
|
||||
origin_asn_count=1,
|
||||
peer_asn_count=1,
|
||||
recent_15m_observation_count=1,
|
||||
recent_24h_observation_count=1,
|
||||
recent_7d_observation_count=1,
|
||||
recent_15m_prefix_count=1,
|
||||
recent_24h_prefix_count=1,
|
||||
recent_7d_prefix_count=1,
|
||||
latest_observed_at=now,
|
||||
)
|
||||
latest = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
latest_event_type="announcement",
|
||||
country="Netherlands",
|
||||
city="Amsterdam",
|
||||
)
|
||||
top_event = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
event_type="announcement",
|
||||
count=1,
|
||||
)
|
||||
scope = SimpleNamespace(
|
||||
collector="rrc00",
|
||||
country="Netherlands",
|
||||
city="Amsterdam",
|
||||
)
|
||||
db = _FakeAsyncSession(
|
||||
[[aggregate], [latest], [top_event], [scope], [aggregate], [latest], [top_event], [scope]]
|
||||
)
|
||||
db = _FakeAsyncSession([[observation], [observation]])
|
||||
client = await _bgp_test_client(db)
|
||||
|
||||
try:
|
||||
|
||||
149
backend/tests/test_bgp_collector_locations.py
Normal file
149
backend/tests/test_bgp_collector_locations.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Tests for the BGP collector + event location services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services import bgp_collector_locations
|
||||
from app.services.bgp_collector_locations import (
|
||||
RIPE_RIS_COLLECTOR_COORDS,
|
||||
collect_bgp_collector_location_candidates,
|
||||
iter_known_collector_names,
|
||||
resolve_bgp_collector_location,
|
||||
)
|
||||
from app.services.bgp_event_locations import (
|
||||
resolve_bgp_event_geo_dict,
|
||||
resolve_bgp_event_location,
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_dict_view_preserves_backward_compatible_keys():
|
||||
rrc00 = RIPE_RIS_COLLECTOR_COORDS["rrc00"]
|
||||
assert rrc00["city"] == "Amsterdam"
|
||||
assert rrc00["country"] == "Netherlands"
|
||||
assert rrc00["latitude"] == pytest.approx(52.3676)
|
||||
assert rrc00["longitude"] == pytest.approx(4.9041)
|
||||
# New richer fields layered on top.
|
||||
assert rrc00["precision"] == "city"
|
||||
assert rrc00["source"] == "legacy_seed"
|
||||
assert rrc00["needs_confirmation"] is True
|
||||
|
||||
|
||||
def test_every_legacy_collector_present():
|
||||
expected = {
|
||||
"rrc00", "rrc01", "rrc03", "rrc04", "rrc05", "rrc06", "rrc07",
|
||||
"rrc10", "rrc11", "rrc12", "rrc13", "rrc14", "rrc15", "rrc16",
|
||||
"rrc18", "rrc19", "rrc20", "rrc21", "rrc22", "rrc23", "rrc24",
|
||||
"rrc25", "rrc26",
|
||||
}
|
||||
assert set(iter_known_collector_names()) == expected
|
||||
|
||||
|
||||
def test_resolve_bgp_collector_returns_stored_location():
|
||||
result = resolve_bgp_collector_location("rrc12")
|
||||
assert result.location is not None
|
||||
assert result.location.city == "Frankfurt"
|
||||
assert result.location.country == "Germany"
|
||||
assert result.location.precision == "city"
|
||||
assert result.location.source == "legacy_seed"
|
||||
assert result.location.needs_confirmation is True
|
||||
|
||||
|
||||
def test_resolve_unknown_bgp_collector_returns_diagnostic(monkeypatch):
|
||||
monkeypatch.setattr(bgp_collector_locations, "_geocode_online", lambda q: None)
|
||||
result = resolve_bgp_collector_location("rrc-doesnotexist")
|
||||
assert result.location is None
|
||||
assert result.diagnostic is not None
|
||||
assert result.diagnostic.failure_reason
|
||||
|
||||
|
||||
def test_collect_bgp_collector_candidates_uses_stored_context_without_registry(monkeypatch):
|
||||
bgp_collector_locations._geocode_online.cache_clear()
|
||||
|
||||
def _fake_geocode(query):
|
||||
assert "CIXP" in query or "Geneva" in query
|
||||
return {
|
||||
"lat": "46.2044",
|
||||
"lon": "6.1432",
|
||||
"display_name": "Geneva, Switzerland",
|
||||
"address": {"city": "Geneva", "country": "Switzerland"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(bgp_collector_locations, "_geocode_online", _fake_geocode)
|
||||
candidates, attempted = collect_bgp_collector_location_candidates(
|
||||
collector="rrc04",
|
||||
)
|
||||
assert attempted, "stored context should feed online query attempts"
|
||||
assert candidates, "online geocoding should produce at least one candidate"
|
||||
best = candidates[0]
|
||||
assert best.source == "nominatim_online_geocode"
|
||||
assert best.needs_confirmation is True
|
||||
assert all(candidate.source != "local_registry" for candidate in candidates)
|
||||
|
||||
|
||||
def test_collect_bgp_collector_candidates_uses_nominatim_when_registry_misses(monkeypatch):
|
||||
bgp_collector_locations._geocode_online.cache_clear()
|
||||
|
||||
def _fake_geocode(query):
|
||||
if "Lyon" not in query and "France-IX" not in query and "FR-IX" not in query:
|
||||
return None
|
||||
return {
|
||||
"lat": "45.764",
|
||||
"lon": "4.8357",
|
||||
"display_name": "Lyon, Auvergne-Rhône-Alpes, France",
|
||||
"address": {"city": "Lyon", "country": "France"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(bgp_collector_locations, "_geocode_online", _fake_geocode)
|
||||
candidates, attempted = collect_bgp_collector_location_candidates(
|
||||
collector="rrc-mystery",
|
||||
city="Lyon",
|
||||
country="France",
|
||||
)
|
||||
assert attempted, "Nominatim plan should run"
|
||||
online = [c for c in candidates if c.source == "nominatim_online_geocode"]
|
||||
assert online, "online resolver must produce a candidate when registry misses"
|
||||
assert online[0].needs_confirmation is True
|
||||
|
||||
|
||||
# ── BGP event resolver ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_event_resolver_inherits_from_owning_collector():
|
||||
geo = resolve_bgp_event_geo_dict("rrc25")
|
||||
assert geo["city"] == "Amsterdam"
|
||||
assert geo["country"] == "Netherlands"
|
||||
assert geo["source"] == "inherited_from_collector"
|
||||
assert geo["precision"] == "city"
|
||||
|
||||
|
||||
def test_event_resolver_does_not_match_unrelated_collectors():
|
||||
"""Regression: passing operator=RIPE NCC must NOT make every collector match."""
|
||||
rrc12 = resolve_bgp_event_geo_dict("rrc12")
|
||||
rrc25 = resolve_bgp_event_geo_dict("rrc25")
|
||||
assert rrc12["city"] == "Frankfurt"
|
||||
assert rrc25["city"] == "Amsterdam"
|
||||
assert rrc12["latitude"] != rrc25["latitude"]
|
||||
|
||||
|
||||
def test_event_resolver_uses_source_coordinates_when_present():
|
||||
geo = resolve_bgp_event_geo_dict(
|
||||
"rrc12",
|
||||
source_latitude=12.34,
|
||||
source_longitude=56.78,
|
||||
)
|
||||
assert geo["latitude"] == pytest.approx(12.34)
|
||||
assert geo["longitude"] == pytest.approx(56.78)
|
||||
assert geo["precision"] == "precise"
|
||||
assert geo["source"] == "source_coordinates"
|
||||
|
||||
|
||||
def test_event_resolver_returns_empty_for_unknown_collector_without_source_coords():
|
||||
geo = resolve_bgp_event_geo_dict("rrc-doesnotexist")
|
||||
assert geo == {}
|
||||
|
||||
|
||||
def test_event_resolver_full_result_carries_diagnostic_on_miss():
|
||||
result = resolve_bgp_event_location(collector="rrc-doesnotexist")
|
||||
assert result.location is None
|
||||
assert result.diagnostic is not None
|
||||
115
backend/tests/test_docs_gatekeeper.py
Normal file
115
backend/tests/test_docs_gatekeeper.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Docs Gatekeeper API tests."""
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.api.v1 import docs as docs_api
|
||||
from app.main import app
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def make_user(role: str = "viewer", groups: list[str] | None = None) -> User:
|
||||
user = User(
|
||||
id=1,
|
||||
username="docs-user",
|
||||
email="docs@example.com",
|
||||
password_hash="x",
|
||||
role=role,
|
||||
is_active=True,
|
||||
)
|
||||
user.gatekeeper_groups = groups or []
|
||||
return user
|
||||
|
||||
|
||||
async def get_json(path: str, user: User | None = None):
|
||||
if user is not None:
|
||||
async def override_user():
|
||||
return user
|
||||
|
||||
app.dependency_overrides[docs_api.get_optional_current_user] = override_user
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
return await client.get(path)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_catalog_only_for_anonymous_user():
|
||||
response = await get_json("/api/v1/docs/catalog")
|
||||
|
||||
assert response.status_code == 200
|
||||
items = response.json()["items"]
|
||||
assert {item["access"] for item in items} == {"public"}
|
||||
assert {item["slug"] for item in items if item["lang"] == "zh"} == {
|
||||
"overview",
|
||||
"quickstart",
|
||||
"manual",
|
||||
"location-pipeline-user",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anonymous_can_read_public_doc():
|
||||
response = await get_json("/api/v1/docs/zh/quickstart")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["access"] == "public"
|
||||
assert "快速开始" in response.json()["markdown"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anonymous_protected_doc_requires_authentication():
|
||||
response = await get_json("/api/v1/docs/zh/backend-collectors")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_viewer_without_group_cannot_read_developer_doc():
|
||||
response = await get_json(
|
||||
"/api/v1/docs/zh/backend-collectors",
|
||||
make_user(role="viewer"),
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_developer_group_can_read_developer_but_not_admin_doc():
|
||||
user = make_user(role="viewer", groups=["docs_developer"])
|
||||
|
||||
developer_response = await get_json("/api/v1/docs/zh/backend-collectors", user)
|
||||
admin_response = await get_json("/api/v1/docs/zh/backend-system-service-control", user)
|
||||
|
||||
assert developer_response.status_code == 200
|
||||
assert developer_response.json()["access"] == "docs_developer"
|
||||
assert admin_response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_and_super_admin_can_read_admin_docs():
|
||||
admin_response = await get_json(
|
||||
"/api/v1/docs/zh/backend-system-service-control",
|
||||
make_user(role="admin"),
|
||||
)
|
||||
super_admin_response = await get_json(
|
||||
"/api/v1/docs/zh/backend-system-service-control",
|
||||
make_user(role="super_admin"),
|
||||
)
|
||||
|
||||
assert admin_response.status_code == 200
|
||||
assert super_admin_response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_language_slug_and_path_traversal_do_not_read_files():
|
||||
bad_lang = await get_json("/api/v1/docs/fr/quickstart")
|
||||
bad_slug = await get_json("/api/v1/docs/zh/not-a-doc")
|
||||
traversal = await get_json("/api/v1/docs/zh/..%2Fmanual")
|
||||
|
||||
assert bad_lang.status_code == 404
|
||||
assert bad_slug.status_code == 404
|
||||
assert traversal.status_code == 404
|
||||
429
backend/tests/test_location_pipeline.py
Normal file
429
backend/tests/test_location_pipeline.py
Normal file
@@ -0,0 +1,429 @@
|
||||
"""Tests for the shared location resolution pipeline.
|
||||
|
||||
Validates the abstraction itself: the protocol contract, the orchestrator,
|
||||
each built-in resolver, and the pluggability promise (a custom resolver can
|
||||
be slotted in without touching consumers).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.location import (
|
||||
InheritFromAnotherEntityResolver,
|
||||
LocationCandidate,
|
||||
LocationPipeline,
|
||||
LocationQuery,
|
||||
NominatimResolver,
|
||||
RegistryResolver,
|
||||
ResolverOutput,
|
||||
SourceCoordinatesResolver,
|
||||
)
|
||||
|
||||
|
||||
# ── Test fixtures ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_registry(tmp_path: Path) -> Path:
|
||||
payload = {
|
||||
"locations": [
|
||||
{
|
||||
"canonical_name": "Test Site Alpha",
|
||||
"aliases": ["alpha", "alpha-one", "Acme HQ"],
|
||||
"operator": "Acme Networks",
|
||||
"site": "Acme HQ",
|
||||
"city": "Lyon",
|
||||
"country": "France",
|
||||
"latitude": 45.764,
|
||||
"longitude": 4.8357,
|
||||
"precision": "site",
|
||||
"confidence": 0.92,
|
||||
"source_note": "Test fixture",
|
||||
"verified_at": "2026-05-08",
|
||||
},
|
||||
{
|
||||
"canonical_name": "Test Site Bravo",
|
||||
"aliases": ["bravo"],
|
||||
"operator": "Acme Networks",
|
||||
"site": "Bravo POP",
|
||||
"city": "Berlin",
|
||||
"country": "Germany",
|
||||
"latitude": 52.52,
|
||||
"longitude": 13.405,
|
||||
"precision": "city",
|
||||
"confidence": 0.85,
|
||||
},
|
||||
],
|
||||
"city_fallbacks": [
|
||||
{
|
||||
"city": "Bhutan-Capital",
|
||||
"country": "Bhutan",
|
||||
"latitude": 27.4728,
|
||||
"longitude": 89.639,
|
||||
"precision": "city",
|
||||
"confidence": 0.5,
|
||||
}
|
||||
],
|
||||
}
|
||||
path = tmp_path / "registry.json"
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
# ── SourceCoordinatesResolver ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_source_coordinates_resolver_passes_through_valid_coordinates():
|
||||
resolver = SourceCoordinatesResolver()
|
||||
query = LocationQuery(
|
||||
name="Acme HQ",
|
||||
source_latitude=45.0,
|
||||
source_longitude=4.0,
|
||||
country="France",
|
||||
)
|
||||
output = resolver.resolve(query)
|
||||
assert len(output.candidates) == 1
|
||||
candidate = output.candidates[0]
|
||||
assert candidate.latitude == 45.0
|
||||
assert candidate.longitude == 4.0
|
||||
assert candidate.precision == "precise"
|
||||
assert candidate.source == "source_coordinates"
|
||||
assert candidate.needs_confirmation is False
|
||||
|
||||
|
||||
def test_source_coordinates_resolver_skips_zero_coordinates():
|
||||
resolver = SourceCoordinatesResolver()
|
||||
output = resolver.resolve(
|
||||
LocationQuery(name="X", source_latitude=0.0, source_longitude=0.0)
|
||||
)
|
||||
assert output.candidates == ()
|
||||
|
||||
|
||||
def test_source_coordinates_resolver_skips_when_missing():
|
||||
resolver = SourceCoordinatesResolver()
|
||||
output = resolver.resolve(LocationQuery(name="X"))
|
||||
assert output.candidates == ()
|
||||
|
||||
|
||||
# ── RegistryResolver ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_registry_resolver_matches_alias(tmp_registry):
|
||||
resolver = RegistryResolver(registry_path=tmp_registry)
|
||||
resolver.reload()
|
||||
output = resolver.resolve(
|
||||
LocationQuery(name="alpha", country="France")
|
||||
)
|
||||
candidates = list(output.candidates)
|
||||
assert candidates, "should match registry entry"
|
||||
assert any(c.matched_location_name == "Test Site Alpha" for c in candidates)
|
||||
alpha = next(c for c in candidates if c.matched_location_name == "Test Site Alpha")
|
||||
assert alpha.precision == "site"
|
||||
assert alpha.confidence == pytest.approx(0.92)
|
||||
assert alpha.needs_confirmation is True
|
||||
assert alpha.location_verified_at is None
|
||||
|
||||
|
||||
def test_registry_resolver_filters_country_mismatch(tmp_registry):
|
||||
resolver = RegistryResolver(registry_path=tmp_registry)
|
||||
resolver.reload()
|
||||
# alpha is in France; query says Spain → should reject
|
||||
output = resolver.resolve(
|
||||
LocationQuery(name="alpha", country="Spain")
|
||||
)
|
||||
assert all(
|
||||
c.matched_location_name != "Test Site Alpha" for c in output.candidates
|
||||
)
|
||||
|
||||
|
||||
def test_registry_resolver_emits_city_fallback_candidate(tmp_registry):
|
||||
resolver = RegistryResolver(registry_path=tmp_registry)
|
||||
resolver.reload()
|
||||
output = resolver.resolve(
|
||||
LocationQuery(city="Bhutan-Capital", country="Bhutan")
|
||||
)
|
||||
candidates = list(output.candidates)
|
||||
assert candidates, "city fallback should fire"
|
||||
assert any(c.source == "local_registry_city" for c in candidates)
|
||||
|
||||
|
||||
# ── NominatimResolver ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_nominatim_resolver_calls_geocoder_with_plan_queries():
|
||||
calls = []
|
||||
|
||||
def fake_geocoder(query: str):
|
||||
calls.append(query)
|
||||
return {
|
||||
"lat": "12.34",
|
||||
"lon": "56.78",
|
||||
"display_name": "Test City, Country",
|
||||
"address": {"city": "Test City", "country": "Country"},
|
||||
}
|
||||
|
||||
def plan(query: LocationQuery):
|
||||
return [
|
||||
("primary query", ("name",)),
|
||||
("secondary query", ("city",)),
|
||||
]
|
||||
|
||||
resolver = NominatimResolver(
|
||||
query_plan_builder=plan,
|
||||
geocoder=fake_geocoder,
|
||||
)
|
||||
output = resolver.resolve(LocationQuery(name="X", country="Country"))
|
||||
assert calls == ["primary query", "secondary query"]
|
||||
assert output.attempted_queries == ("primary query", "secondary query")
|
||||
assert len(output.candidates) == 2
|
||||
assert all(c.precision == "city" for c in output.candidates)
|
||||
assert all(c.needs_confirmation for c in output.candidates)
|
||||
|
||||
|
||||
def test_nominatim_resolver_skips_when_geocoder_returns_none():
|
||||
resolver = NominatimResolver(
|
||||
query_plan_builder=lambda q: [("only", ("name",))],
|
||||
geocoder=lambda q: None,
|
||||
)
|
||||
output = resolver.resolve(LocationQuery(name="X"))
|
||||
assert output.candidates == ()
|
||||
assert output.attempted_queries == ("only",)
|
||||
|
||||
|
||||
def test_nominatim_resolver_swallows_exceptions_per_query():
|
||||
def boom(query):
|
||||
raise RuntimeError("network down")
|
||||
|
||||
resolver = NominatimResolver(
|
||||
query_plan_builder=lambda q: [("a", ()), ("b", ())],
|
||||
geocoder=boom,
|
||||
)
|
||||
output = resolver.resolve(LocationQuery(name="X"))
|
||||
assert output.candidates == ()
|
||||
assert output.attempted_queries == ("a", "b")
|
||||
|
||||
|
||||
# ── InheritFromAnotherEntityResolver ────────────────────────────────
|
||||
|
||||
|
||||
def test_inherit_resolver_returns_provided_candidate():
|
||||
sentinel = LocationCandidate(
|
||||
latitude=10.0,
|
||||
longitude=20.0,
|
||||
display_name="Inherited",
|
||||
precision="city",
|
||||
confidence=0.7,
|
||||
query="inherit::test",
|
||||
source="inherited",
|
||||
source_note=None,
|
||||
matched_fields=("collector",),
|
||||
needs_confirmation=False,
|
||||
)
|
||||
resolver = InheritFromAnotherEntityResolver(
|
||||
source_lookup=lambda q: sentinel
|
||||
)
|
||||
output = resolver.resolve(LocationQuery(name="X"))
|
||||
assert output.candidates == (sentinel,)
|
||||
|
||||
|
||||
def test_inherit_resolver_skips_when_lookup_returns_none():
|
||||
resolver = InheritFromAnotherEntityResolver(source_lookup=lambda q: None)
|
||||
assert resolver.resolve(LocationQuery(name="X")).candidates == ()
|
||||
|
||||
|
||||
# ── LocationPipeline orchestration ──────────────────────────────────
|
||||
|
||||
|
||||
def test_pipeline_aggregates_candidates_across_resolvers(tmp_registry):
|
||||
pipeline = LocationPipeline(
|
||||
[
|
||||
SourceCoordinatesResolver(),
|
||||
RegistryResolver(registry_path=tmp_registry),
|
||||
NominatimResolver(
|
||||
query_plan_builder=lambda q: [("nominatim attempt", ("name",))],
|
||||
geocoder=lambda q: {
|
||||
"lat": "1.0",
|
||||
"lon": "2.0",
|
||||
"display_name": "Online City",
|
||||
"address": {"city": "Online City", "country": "France"},
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
pipeline.resolvers[1].reload()
|
||||
candidates, attempted = pipeline.collect_candidates(
|
||||
LocationQuery(
|
||||
name="alpha",
|
||||
country="France",
|
||||
source_latitude=44.0,
|
||||
source_longitude=5.0,
|
||||
)
|
||||
)
|
||||
sources = {c.source for c in candidates}
|
||||
assert "source_coordinates" in sources
|
||||
assert "local_registry" in sources
|
||||
assert "nominatim_online_geocode" in sources
|
||||
assert "nominatim attempt" in attempted
|
||||
|
||||
|
||||
def test_pipeline_dedupes_by_source_and_coordinates():
|
||||
same = LocationCandidate(
|
||||
latitude=1.0,
|
||||
longitude=2.0,
|
||||
display_name="dup",
|
||||
precision="city",
|
||||
confidence=0.5,
|
||||
query="x",
|
||||
source="dup_source",
|
||||
source_note=None,
|
||||
matched_fields=(),
|
||||
needs_confirmation=False,
|
||||
)
|
||||
|
||||
class _DupResolver:
|
||||
name = "dup_source"
|
||||
|
||||
def resolve(self, query):
|
||||
return ResolverOutput(candidates=(same, same))
|
||||
|
||||
pipeline = LocationPipeline([_DupResolver()])
|
||||
candidates, _ = pipeline.collect_candidates(LocationQuery(name="X"))
|
||||
assert len(candidates) == 1
|
||||
|
||||
|
||||
def test_registry_short_aliases_do_not_match_inside_larger_tokens(tmp_path: Path):
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"locations": [
|
||||
{
|
||||
"canonical_name": "Aurora",
|
||||
"aliases": ["Aurora", "ANL"],
|
||||
"site": "DOE/SC/Argonne National Laboratory",
|
||||
"country": "United States",
|
||||
"city": "Lemont",
|
||||
"latitude": 41.713,
|
||||
"longitude": -87.982,
|
||||
"precision": "site",
|
||||
},
|
||||
{
|
||||
"canonical_name": "Venado",
|
||||
"aliases": ["Venado"],
|
||||
"site": "DOE/NNSA/LANL",
|
||||
"country": "United States",
|
||||
"city": "Los Alamos",
|
||||
"latitude": 35.8443,
|
||||
"longitude": -106.2872,
|
||||
"precision": "site",
|
||||
},
|
||||
],
|
||||
"city_fallbacks": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
resolver = RegistryResolver(registry_path=registry_path)
|
||||
resolver.reload()
|
||||
|
||||
output = resolver.resolve(
|
||||
LocationQuery(
|
||||
name="Venado",
|
||||
country="United States",
|
||||
extra={"site": "DOE/NNSA/LANL"},
|
||||
)
|
||||
)
|
||||
|
||||
assert len(output.candidates) == 1
|
||||
assert output.candidates[0].matched_location_name == "Venado"
|
||||
|
||||
|
||||
def test_pipeline_resolve_best_returns_highest_priority():
|
||||
online = LocationCandidate(
|
||||
latitude=10.0,
|
||||
longitude=20.0,
|
||||
display_name="online",
|
||||
precision="city",
|
||||
confidence=0.9,
|
||||
query="x",
|
||||
source="nominatim_online_geocode",
|
||||
source_note=None,
|
||||
matched_fields=(),
|
||||
needs_confirmation=True,
|
||||
)
|
||||
source = LocationCandidate(
|
||||
latitude=11.0,
|
||||
longitude=21.0,
|
||||
display_name="src",
|
||||
precision="precise",
|
||||
confidence=1.0,
|
||||
query="x",
|
||||
source="source_coordinates",
|
||||
source_note=None,
|
||||
matched_fields=(),
|
||||
needs_confirmation=False,
|
||||
)
|
||||
|
||||
class _StubResolver:
|
||||
def __init__(self, c, name):
|
||||
self._c = c
|
||||
self.name = name
|
||||
|
||||
def resolve(self, query):
|
||||
return ResolverOutput(candidates=(self._c,))
|
||||
|
||||
pipeline = LocationPipeline(
|
||||
[
|
||||
_StubResolver(online, "online"),
|
||||
_StubResolver(source, "src"),
|
||||
]
|
||||
)
|
||||
result = pipeline.resolve_best(LocationQuery(name="X"))
|
||||
assert result.location is source, "source_coordinates should beat nominatim"
|
||||
|
||||
|
||||
def test_pipeline_returns_diagnostic_when_nothing_resolves():
|
||||
pipeline = LocationPipeline([SourceCoordinatesResolver()])
|
||||
result = pipeline.resolve_best(LocationQuery(name="X", country="Bhutan"))
|
||||
assert result.location is None
|
||||
assert result.diagnostic is not None
|
||||
assert result.diagnostic.country == "Bhutan"
|
||||
|
||||
|
||||
def test_pluggability_custom_resolver_works_without_changing_pipeline():
|
||||
"""Validates the abstraction promise: a new algorithm = a new class."""
|
||||
|
||||
class _PeeringDBStubResolver:
|
||||
name = "fake_peeringdb"
|
||||
|
||||
def resolve(self, query):
|
||||
asn = (query.extra or {}).get("asn")
|
||||
if asn != 174:
|
||||
return ResolverOutput()
|
||||
return ResolverOutput(
|
||||
candidates=(
|
||||
LocationCandidate(
|
||||
latitude=1.0,
|
||||
longitude=2.0,
|
||||
display_name="Cogent HQ",
|
||||
precision="site",
|
||||
confidence=0.8,
|
||||
query=f"peeringdb::{asn}",
|
||||
source="peeringdb_stub",
|
||||
source_note="Stub for testing",
|
||||
matched_fields=("asn",),
|
||||
needs_confirmation=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
pipeline = LocationPipeline([_PeeringDBStubResolver()])
|
||||
candidates, _ = pipeline.collect_candidates(
|
||||
LocationQuery(name="X", extra={"asn": 174})
|
||||
)
|
||||
assert len(candidates) == 1
|
||||
assert candidates[0].source == "peeringdb_stub"
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.api.v1.visualization import convert_compute_centers_to_geojson
|
||||
import app.services.compute_center_locations as compute_center_locations
|
||||
from app.db.session import get_db
|
||||
from app.main import app
|
||||
from app.models.collected_data import CollectedData
|
||||
@@ -89,6 +90,8 @@ def test_convert_compute_centers_to_geojson_unifies_sources():
|
||||
assert supercomputer_feature["properties"]["operator"] == "ORNL"
|
||||
assert supercomputer_feature["properties"]["location_precision"] == "precise"
|
||||
assert supercomputer_feature["properties"]["is_estimated"] is False
|
||||
assert supercomputer_feature["properties"]["location_source"] == "source_coordinates"
|
||||
assert supercomputer_feature["properties"]["location_confidence"] == 1.0
|
||||
|
||||
gpu_feature = payload["features"][1]
|
||||
assert gpu_feature["properties"]["site_type"] == "gpu_cluster"
|
||||
@@ -98,8 +101,110 @@ def test_convert_compute_centers_to_geojson_unifies_sources():
|
||||
assert gpu_feature["properties"]["location_precision"] == "precise"
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_uses_coordinate_hints():
|
||||
hinted_record = _build_record(
|
||||
def test_convert_compute_centers_to_geojson_accepts_source_coordinate_aliases():
|
||||
record = _build_record(
|
||||
record_id=3,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Alias Coordinates",
|
||||
country="United States",
|
||||
city="New York",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={
|
||||
"latitude": "",
|
||||
"longitude": "",
|
||||
"location": {
|
||||
"lat": 40.7128,
|
||||
"lng": -74.0060,
|
||||
},
|
||||
"value": "1200",
|
||||
"unit": "TFlop/s",
|
||||
},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([record])
|
||||
|
||||
assert len(payload["features"]) == 1
|
||||
feature = payload["features"][0]
|
||||
assert feature["geometry"]["coordinates"] == [-74.006, 40.7128]
|
||||
assert feature["properties"]["location_source"] == "source_coordinates"
|
||||
|
||||
|
||||
def test_compute_center_source_coordinates_win_over_stored_location():
|
||||
compute_center_locations.set_compute_center_location_cache({
|
||||
"top500:top500-31": {
|
||||
"source": "top500",
|
||||
"source_id": "top500-31",
|
||||
"name": "Stored Wrong",
|
||||
"latitude": 1.0,
|
||||
"longitude": 2.0,
|
||||
"precision": "city",
|
||||
"confidence": 0.5,
|
||||
"needs_confirmation": True,
|
||||
}
|
||||
})
|
||||
record = _build_record(
|
||||
record_id=31,
|
||||
source="top500",
|
||||
data_type="supercomputer",
|
||||
name="Source Wins",
|
||||
country="United States",
|
||||
city="Oak Ridge",
|
||||
latitude=35.93,
|
||||
longitude=-84.31,
|
||||
metadata={"organization": "ORNL"},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([record])
|
||||
|
||||
assert payload["features"][0]["geometry"]["coordinates"] == [-84.31, 35.93]
|
||||
assert payload["features"][0]["properties"]["location_source"] == "source_coordinates"
|
||||
compute_center_locations.set_compute_center_location_cache({})
|
||||
|
||||
|
||||
def test_compute_center_geojson_uses_stored_location_when_source_coords_missing():
|
||||
compute_center_locations.set_compute_center_location_cache({
|
||||
"epoch_ai_gpu:epoch_ai_gpu-32": {
|
||||
"source": "epoch_ai_gpu",
|
||||
"source_id": "epoch_ai_gpu-32",
|
||||
"name": "Stored Cluster",
|
||||
"city": "Memphis",
|
||||
"country": "United States",
|
||||
"latitude": 35.1495,
|
||||
"longitude": -90.049,
|
||||
"precision": "city",
|
||||
"confidence": 0.72,
|
||||
"location_source": "manual_selection",
|
||||
"source_note": "Saved by user",
|
||||
"needs_confirmation": False,
|
||||
"verified_at": "2026-05-08T00:00:00Z",
|
||||
}
|
||||
})
|
||||
record = _build_record(
|
||||
record_id=32,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Stored Cluster",
|
||||
country="United States",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"value": "1200", "unit": "TFlop/s"},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([record])
|
||||
|
||||
assert len(payload["features"]) == 1
|
||||
feature = payload["features"][0]
|
||||
assert feature["geometry"]["coordinates"] == [-90.049, 35.1495]
|
||||
assert feature["properties"]["location_source"] == "stored_compute_center_location"
|
||||
assert feature["properties"]["needs_confirmation"] is False
|
||||
compute_center_locations.set_compute_center_location_cache({})
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_does_not_use_registry_aliases():
|
||||
registry_record = _build_record(
|
||||
record_id=3,
|
||||
source="top500",
|
||||
data_type="supercomputer",
|
||||
@@ -114,23 +219,51 @@ def test_convert_compute_centers_to_geojson_uses_coordinate_hints():
|
||||
},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([hinted_record])
|
||||
payload = convert_compute_centers_to_geojson([registry_record])
|
||||
|
||||
assert len(payload["features"]) == 1
|
||||
coords = payload["features"][0]["geometry"]["coordinates"]
|
||||
assert coords[0] == pytest.approx(-84.3107)
|
||||
assert coords[1] == pytest.approx(35.9319)
|
||||
assert payload["features"][0]["properties"]["is_estimated"] is True
|
||||
assert payload["features"][0]["properties"]["location_precision"] == "estimated_site"
|
||||
assert payload["features"] == []
|
||||
assert len(payload["unresolved"]) == 1
|
||||
assert payload["unresolved"][0]["name"] == "Frontier"
|
||||
assert "source coords" in payload["unresolved"][0]["failure_reason"]
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_falls_back_to_country_centroid():
|
||||
centroid_record = _build_record(
|
||||
def test_convert_compute_centers_to_geojson_does_not_use_city_fallback():
|
||||
city_record = _build_record(
|
||||
record_id=4,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Sample GPU Cluster",
|
||||
country="United States",
|
||||
city="San Francisco, CA",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={
|
||||
"organization": "Sample Operator",
|
||||
"value": "10000",
|
||||
"unit": "TFlop/s",
|
||||
},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([city_record])
|
||||
|
||||
assert payload["features"] == []
|
||||
assert len(payload["unresolved"]) == 1
|
||||
assert payload["unresolved"][0]["city"] == "San Francisco, CA"
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_does_not_online_geocode_on_startup(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
|
||||
def _explode(_query):
|
||||
raise AssertionError("startup GeoJSON must not call online geocoding")
|
||||
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", _explode)
|
||||
country_record = _build_record(
|
||||
record_id=4,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Unknown Cluster",
|
||||
country="United States",
|
||||
country="France",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
@@ -141,16 +274,228 @@ def test_convert_compute_centers_to_geojson_falls_back_to_country_centroid():
|
||||
},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([centroid_record])
|
||||
payload = convert_compute_centers_to_geojson([country_record])
|
||||
|
||||
assert len(payload["features"]) == 1
|
||||
props = payload["features"][0]["properties"]
|
||||
coords = payload["features"][0]["geometry"]["coordinates"]
|
||||
assert coords[0] == pytest.approx(-98.5795)
|
||||
assert coords[1] == pytest.approx(39.8283)
|
||||
assert props["is_estimated"] is True
|
||||
assert props["location_precision"] == "estimated_country"
|
||||
assert props["geography_mode"] == "country_centroid"
|
||||
assert payload["features"] == []
|
||||
assert len(payload["unresolved"]) == 1
|
||||
assert payload["unresolved"][0]["operator"] == "Unknown Operator"
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_records_diagnostics_when_online_geocode_fails(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
country_record = _build_record(
|
||||
record_id=5,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Unknown French Cluster",
|
||||
country="France",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={
|
||||
"organization": "Unknown Operator",
|
||||
"value": "10000",
|
||||
"unit": "TFlop/s",
|
||||
},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([country_record])
|
||||
|
||||
assert payload["features"] == []
|
||||
assert len(payload["unresolved"]) == 1
|
||||
diagnostic = payload["unresolved"][0]
|
||||
assert diagnostic["record_id"] == 5
|
||||
assert diagnostic["source_id"] == "epoch_ai_gpu-5"
|
||||
assert diagnostic["country"] == "France"
|
||||
assert diagnostic["operator"] == "Unknown Operator"
|
||||
assert diagnostic["failure_reason"]
|
||||
assert diagnostic["attempted_queries"] == []
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_records_diagnostics_when_no_country(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
unknown_record = _build_record(
|
||||
record_id=6,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Unknown Offshore Cluster",
|
||||
country="",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={
|
||||
"organization": "Unknown Operator",
|
||||
"value": "10000",
|
||||
"unit": "TFlop/s",
|
||||
},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([unknown_record])
|
||||
|
||||
assert payload["features"] == []
|
||||
assert len(payload["unresolved"]) == 1
|
||||
assert payload["unresolved"][0]["failure_reason"]
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_never_emits_zero_coordinates(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
|
||||
def _zero_geocode(query):
|
||||
return {
|
||||
"lat": "0",
|
||||
"lon": "0",
|
||||
"display_name": "Null Island",
|
||||
"address": {"city": "", "country": ""},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", _zero_geocode)
|
||||
record = _build_record(
|
||||
record_id=7,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Null Island Cluster",
|
||||
country="",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"organization": "Null Inc"},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([record])
|
||||
|
||||
for feature in payload["features"]:
|
||||
coords = feature["geometry"]["coordinates"]
|
||||
assert coords[0] not in (0, 0.0)
|
||||
assert coords[1] not in (0, 0.0)
|
||||
|
||||
|
||||
def test_convert_compute_centers_to_geojson_rejects_country_or_unknown_precision(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
record = _build_record(
|
||||
record_id=8,
|
||||
source="top500",
|
||||
data_type="supercomputer",
|
||||
name="Phantom System",
|
||||
country="Liechtenstein",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"organization": "Phantom Operator", "rmax": 100.0},
|
||||
)
|
||||
|
||||
payload = convert_compute_centers_to_geojson([record])
|
||||
|
||||
for feature in payload["features"]:
|
||||
assert feature["properties"]["location_precision"] in {"precise", "site", "city"}
|
||||
assert payload["features"] == []
|
||||
assert payload["unresolved"], "phantom record must surface as diagnostic"
|
||||
|
||||
|
||||
def test_resolve_full_returns_diagnostic_for_unresolved(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
record = _build_record(
|
||||
record_id=11,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Phantom Cluster",
|
||||
country="Bhutan",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"organization": "Mystery Operator"},
|
||||
)
|
||||
result = compute_center_locations.resolve_compute_center_location_full(record, record.extra_data)
|
||||
assert result.location is None
|
||||
assert result.diagnostic is not None
|
||||
assert result.diagnostic.failure_reason
|
||||
assert result.diagnostic.country == "Bhutan"
|
||||
|
||||
|
||||
def test_collect_location_candidates_ignores_registry_and_uses_online(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
|
||||
def _fake_ror(query):
|
||||
assert query == "Oak Ridge National Laboratory"
|
||||
return {
|
||||
"id": "https://ror.org/01qz5mb56",
|
||||
"names": [
|
||||
{"types": ["ror_display"], "value": "Oak Ridge National Laboratory"}
|
||||
],
|
||||
"locations": [
|
||||
{
|
||||
"geonames_id": 4646571,
|
||||
"geonames_details": {
|
||||
"name": "Oak Ridge",
|
||||
"country_subdivision_name": "Tennessee",
|
||||
"country_name": "United States",
|
||||
"lat": 36.01036,
|
||||
"lng": -84.26964,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(compute_center_locations, "_lookup_ror_organization", _fake_ror)
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
candidates, attempted = compute_center_locations.collect_location_candidates(
|
||||
name="Frontier",
|
||||
operator="Oak Ridge National Laboratory",
|
||||
country="United States",
|
||||
)
|
||||
assert candidates, "online source-traced query must produce a candidate"
|
||||
best = candidates[0]
|
||||
assert best.source == "ror_organization_registry"
|
||||
assert best.precision == "city"
|
||||
assert best.needs_confirmation is True
|
||||
assert attempted[0] == "ror:Oak Ridge National Laboratory"
|
||||
|
||||
|
||||
def test_collect_location_candidates_returns_online_when_registry_misses(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
|
||||
def _fake_geocode(query):
|
||||
if "Lyon" not in query and "Mystery Operator" not in query and "Lyon, France" not in query:
|
||||
return None
|
||||
return {
|
||||
"lat": "45.7640",
|
||||
"lon": "4.8357",
|
||||
"display_name": "Lyon, Auvergne-Rhône-Alpes, France",
|
||||
"address": {"city": "Lyon", "state": "Auvergne-Rhône-Alpes", "country": "France"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", _fake_geocode)
|
||||
monkeypatch.setattr(compute_center_locations, "_lookup_ror_organization", lambda _query: None)
|
||||
candidates, attempted = compute_center_locations.collect_location_candidates(
|
||||
name="Mystery System",
|
||||
operator="Mystery Operator",
|
||||
city="Lyon",
|
||||
country="France",
|
||||
)
|
||||
assert candidates, "online geocoding must produce a candidate"
|
||||
online_candidates = [c for c in candidates if c.source == "nominatim_online_geocode"]
|
||||
assert online_candidates, "must include at least one online candidate"
|
||||
online = online_candidates[0]
|
||||
assert online.precision == "city"
|
||||
assert online.needs_confirmation is True
|
||||
assert online.suggested_registry_entry is not None
|
||||
assert attempted, "must record attempted query strings"
|
||||
|
||||
|
||||
def test_collect_location_candidates_failure_returns_attempted_queries(monkeypatch):
|
||||
compute_center_locations._geocode_online.cache_clear()
|
||||
monkeypatch.setattr(compute_center_locations, "_lookup_ror_organization", lambda _query: None)
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
candidates, attempted = compute_center_locations.collect_location_candidates(
|
||||
name="Mystery Offshore Cluster",
|
||||
operator="Mystery Operator",
|
||||
country="Bhutan",
|
||||
)
|
||||
assert candidates == []
|
||||
assert attempted, "even on failure we record attempted queries for diagnostics"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -353,3 +698,304 @@ async def test_visualization_geo_summary_returns_counts(monkeypatch):
|
||||
assert stats["bgp_collector_count"] == 2
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_location_endpoint_returns_candidates_for_known_record(monkeypatch):
|
||||
def _fake_ror(query):
|
||||
assert query == "Oak Ridge National Laboratory"
|
||||
return {
|
||||
"id": "https://ror.org/01qz5mb56",
|
||||
"names": [
|
||||
{"types": ["ror_display"], "value": "Oak Ridge National Laboratory"}
|
||||
],
|
||||
"locations": [
|
||||
{
|
||||
"geonames_id": 4646571,
|
||||
"geonames_details": {
|
||||
"name": "Oak Ridge",
|
||||
"country_subdivision_name": "Tennessee",
|
||||
"country_name": "United States",
|
||||
"lat": 36.01036,
|
||||
"lng": -84.26964,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(compute_center_locations, "_lookup_ror_organization", _fake_ror)
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
|
||||
target_record = _build_record(
|
||||
record_id=42,
|
||||
source="top500",
|
||||
data_type="supercomputer",
|
||||
name="Frontier",
|
||||
country="United States",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"organization": "Oak Ridge National Laboratory", "rmax": 1102000.0},
|
||||
)
|
||||
|
||||
class _ScalarResult:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def scalars(self):
|
||||
class _Scalars:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return self._rows
|
||||
|
||||
return _Scalars(self._rows)
|
||||
|
||||
class _FakeSession:
|
||||
async def execute(self, _query):
|
||||
return _ScalarResult([target_record])
|
||||
|
||||
async def override_get_db():
|
||||
yield _FakeSession()
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.post(
|
||||
"/api/v1/visualization/compute-centers/top500-42/collect-location",
|
||||
json={
|
||||
"name": "Frontier",
|
||||
"operator": "Oak Ridge National Laboratory",
|
||||
"country": "United States",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["success"] is True
|
||||
assert body["candidates"], "must include candidates"
|
||||
best = body["best_candidate"]
|
||||
assert best["precision"] in {"precise", "site", "city"}
|
||||
assert best["source"] == "ror_organization_registry"
|
||||
assert best["needs_confirmation"] is True
|
||||
assert best["matched_fields"], "matched_fields must be populated"
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_location_endpoint_returns_failure_reason(monkeypatch):
|
||||
monkeypatch.setattr(compute_center_locations, "_lookup_ror_organization", lambda _query: None)
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
|
||||
class _ScalarResult:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def scalars(self):
|
||||
class _Scalars:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return self._rows
|
||||
|
||||
return _Scalars(self._rows)
|
||||
|
||||
class _FakeSession:
|
||||
async def execute(self, _query):
|
||||
return _ScalarResult([])
|
||||
|
||||
async def override_get_db():
|
||||
yield _FakeSession()
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.post(
|
||||
"/api/v1/visualization/compute-centers/epoch-mystery-99/collect-location",
|
||||
json={
|
||||
"name": "Mystery Cluster",
|
||||
"operator": "Mystery Operator",
|
||||
"country": "Bhutan",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["success"] is False
|
||||
assert body["failure_reason"]
|
||||
assert body["candidates"] == []
|
||||
assert body["attempted_queries"], "must include attempted queries"
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_location_endpoint_upserts_and_geojson_can_render():
|
||||
target_record = _build_record(
|
||||
record_id=52,
|
||||
source="epoch_ai_gpu",
|
||||
data_type="gpu_cluster",
|
||||
name="Saved Cluster",
|
||||
country="United States",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"value": "1200", "unit": "TFlop/s"},
|
||||
)
|
||||
|
||||
class _ScalarResult:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def scalars(self):
|
||||
class _Scalars:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return self._rows
|
||||
|
||||
return _Scalars(self._rows)
|
||||
|
||||
class _FakeSession:
|
||||
def __init__(self):
|
||||
self.saved = []
|
||||
|
||||
async def execute(self, _query):
|
||||
if self.saved:
|
||||
return _ScalarResult(self.saved)
|
||||
return _ScalarResult([target_record])
|
||||
|
||||
async def scalar(self, _query):
|
||||
return None
|
||||
|
||||
def add(self, record):
|
||||
self.saved.append(record)
|
||||
|
||||
async def commit(self):
|
||||
return None
|
||||
|
||||
async def refresh(self, _record):
|
||||
return None
|
||||
|
||||
fake_session = _FakeSession()
|
||||
|
||||
async def override_get_db():
|
||||
yield fake_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.post(
|
||||
"/api/v1/visualization/compute-centers/epoch_ai_gpu-52/location",
|
||||
json={
|
||||
"source": "epoch_ai_gpu",
|
||||
"name": "Saved Cluster",
|
||||
"latitude": 35.1495,
|
||||
"longitude": -90.049,
|
||||
"precision": "city",
|
||||
"confidence": 0.72,
|
||||
"location_source": "ror_organization_registry",
|
||||
"source_note": "Selected by user",
|
||||
"raw_payload": {"source": "ror_organization_registry"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["success"] is True
|
||||
assert fake_session.saved
|
||||
|
||||
payload = convert_compute_centers_to_geojson([target_record])
|
||||
assert len(payload["features"]) == 1
|
||||
feature = payload["features"][0]
|
||||
assert feature["geometry"]["coordinates"] == [-90.049, 35.1495]
|
||||
assert feature["properties"]["location_source"] == "stored_compute_center_location"
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
compute_center_locations.set_compute_center_location_cache({})
|
||||
|
||||
|
||||
def test_resolution_chain_orders_source_coords_first(monkeypatch):
|
||||
def _explode(_query):
|
||||
raise AssertionError("source coords must short-circuit before online geocoding")
|
||||
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", _explode)
|
||||
record = _build_record(
|
||||
record_id=20,
|
||||
source="top500",
|
||||
data_type="supercomputer",
|
||||
name="Frontier",
|
||||
country="United States",
|
||||
city="Oak Ridge",
|
||||
latitude=35.93,
|
||||
longitude=-84.31,
|
||||
metadata={"organization": "ORNL"},
|
||||
)
|
||||
result = compute_center_locations.resolve_compute_center_location_full(record, record.extra_data)
|
||||
assert result.is_resolved
|
||||
assert result.location.location_precision == "precise"
|
||||
assert result.location.location_source == "source_coordinates"
|
||||
|
||||
|
||||
def test_no_country_centroid_or_major_compute_city_fallback(monkeypatch):
|
||||
monkeypatch.setattr(compute_center_locations, "_geocode_online", lambda _query: None)
|
||||
record = _build_record(
|
||||
record_id=21,
|
||||
source="top500",
|
||||
data_type="supercomputer",
|
||||
name="Phantom System",
|
||||
country="France",
|
||||
city="",
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
metadata={"organization": "Phantom Operator"},
|
||||
)
|
||||
result = compute_center_locations.resolve_compute_center_location_full(record, record.extra_data)
|
||||
assert result.location is None, "must NOT fall back to country centroid or hashed major city"
|
||||
assert result.diagnostic is not None
|
||||
assert result.diagnostic.failure_reason
|
||||
|
||||
|
||||
def test_repository_has_no_forbidden_precision_tokens():
|
||||
"""Static guard: forbidden fallback strategies must not regress into the codebase.
|
||||
|
||||
Each forbidden token may appear at most once per target file, and only inside
|
||||
the FORBIDDEN_PRECISIONS guard list (so we still reject them at runtime).
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
backend_root = Path(__file__).resolve().parents[1]
|
||||
forbidden_tokens = (
|
||||
"country_centroid",
|
||||
"country_major_compute_city",
|
||||
"estimated_country",
|
||||
)
|
||||
targets = [
|
||||
backend_root / "app" / "services" / "compute_center_locations.py",
|
||||
backend_root / "app" / "api" / "v1" / "visualization.py",
|
||||
]
|
||||
for target in targets:
|
||||
text = target.read_text(encoding="utf-8")
|
||||
for token in forbidden_tokens:
|
||||
occurrences = text.count(token)
|
||||
assert occurrences <= 1, (
|
||||
f"{token} appears {occurrences} times in {target}; "
|
||||
"should only appear in FORBIDDEN_PRECISIONS guard list."
|
||||
)
|
||||
if occurrences == 1:
|
||||
assert "FORBIDDEN_PRECISIONS" in text, (
|
||||
f"{token} appears in {target} outside the FORBIDDEN_PRECISIONS guard"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user