meshai/work/tests/test_adapter_wzdx.py
Matt Johnson ef2df5f54b chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name
Final structural step. central_normalizer.py (973 LOC, misnamed — it lived
OUTSIDE central/ but its docstring described "the Central firehose") is DELETED.
No file or symbol named "central" survives (only immutable historical comments
in an already-applied migration .sql remain — those are not edited).

Split per the owner's rule ("next to what uses it"):
- Live geo/place-name utilities (nearest_town, _haversine_miles, _bearing_compass,
  _compute_distance_bearing, _h3_cell, _photon_reverse_places, geocoder config)
  → the EXISTING meshai/geo.py ("Geographic utilities…"), same domain. NOT a
  junk drawer. geo.py's nearest_city (hardcoded table) and central_normalizer's
  nearest_town (live Photon+H3 reverse-geocode) are different implementations —
  both kept, no false consolidation.
- WZDx work-zone parsers (_parse_wzdx_federal, _norm_wzdx_sub_type, _norm_direction,
  _parse_mile_posts, _clean_description, _is_uninformative_road) → env/wzdx_parse.py,
  next to their sole consumer env/wzdx.py.
- ~400 dead lines deleted: the Central-envelope machinery (normalize,
  _parse_state_511_atis, _parse_wfigs_incidents, _parse_itd_511_work_zone,
  should_skip_state_511_atis_id, is_incident_envelope_stale, normalize_road_name,
  _norm_sub_type, _parse_ends_at) — only caller was the deleted Central consumer.

Consumers rewired: env/wzdx.py, env/fire_render.py, formatters/fire.py,
formatters/_anchor.py, main.py, persistence/curation.py, composer.py.

Tests: test_central_normalizer.py RENAMED → test_geo_wzdx_parse.py (live geo/wzdx
tests kept + rewired; dead-parser tests dropped). test_itd_511_work_zone.py
deleted (tested only the dead itd_511 parser — verified gone from source).
Other test files: import-path rewires. Live coverage for nearest_town /
_parse_wzdx_federal / bearing preserved across test_geo_wzdx_parse, test_adapter_wzdx,
test_wfigs_handler, test_incident_refactor.

Suite: 2010 passed, 0 failed. The 49-test drop is the dead Central-envelope
parser tests, correctly removed. Stale "Central firehose" docstrings on the
moved code updated to describe present behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:11:26 +00:00

329 lines
12 KiB
Python

"""Tests for the FHWA WZDx native adapter — to_event() + discovery/fetch.
Mirrors the style of test_adapter_firms.py: build realistic WZDx v4 GeoJSON
fixtures, drive the adapter's parse + to_event, and assert the canonical
``work_zone`` data dict is populated correctly and renders through the real
Phase-2 incident formatter. nearest_town() is patched off (no Photon
network) for deterministic town-independent assertions.
"""
import calendar
import time
from types import SimpleNamespace
import pytest
from meshai import geo
from meshai.env.wzdx import WZDxAdapter
from meshai.notifications.events import Event
from meshai.notifications.formatters.incident import format as format_incident
# ============================================================
# FIXTURES
# ============================================================
@pytest.fixture(autouse=True)
def _no_photon(monkeypatch):
"""Disable the Photon nearest_town lookup so parsing is network-free."""
monkeypatch.setattr(geo, "nearest_town", lambda *a, **k: None)
@pytest.fixture
def mock_config():
"""A WZDx config with registry discovery enabled, keyless."""
return SimpleNamespace(
enabled=True,
feed_source="native",
base_url="",
registry_url="https://datahub.transportation.gov/resource/69qe-yiui.json?$limit=200",
registry_ttl=21600,
tick_seconds=300,
states=["ID"],
bbox=[],
)
@pytest.fixture
def adapter(mock_config):
return WZDxAdapter(mock_config)
def make_wzdx_feature(
feat_id="WZ-0001",
data_source_id="idot-1",
road="US-95",
direction="southbound",
event_type="work-zone",
work_type="surface-work",
vehicle_impact="some-lanes-closed",
description="Paving Operations on US-95 from MM (93) to MM (89).",
start_date="2026-07-18T13:00:00Z",
end_date="2026-07-19T00:59:59Z",
coordinates=None,
):
"""Build a realistic WZDx v4 GeoJSON road_event feature."""
if coordinates is None:
coordinates = [[-116.91, 43.66], [-116.92, 43.60]]
core = {"event_type": event_type, "data_source_id": data_source_id}
if road is not None:
core["road_names"] = [road]
if direction is not None:
core["direction"] = direction
if description is not None:
core["description"] = description
props = {"core_details": core}
if work_type is not None:
props["types_of_work"] = [{"type_name": work_type}]
if vehicle_impact is not None:
props["vehicle_impact"] = vehicle_impact
if start_date is not None:
props["start_date"] = start_date
if end_date is not None:
props["end_date"] = end_date
return {
"id": feat_id,
"type": "Feature",
"properties": props,
"geometry": {"type": "LineString", "coordinates": coordinates},
}
def make_feature_collection(*features):
return {"type": "FeatureCollection", "features": list(features)}
# ============================================================
# REGISTRY DISCOVERY
# ============================================================
def test_select_feeds_filters_state_and_geojson(adapter):
"""Registry selection keeps configured-state GeoJSON feeds only."""
rows = [
{"state": "Idaho", "format": "geojson",
"url": "https://itd.idaho.gov/wzdx.geojson", "version": "4.0"},
{"state": "ID", "format": "GeoJSON", "apiurl": "https://alt.idaho/wzdx"},
{"state": "Utah", "format": "geojson", "url": "https://udot/wzdx"},
{"state": "Idaho", "format": "xml", "url": "https://itd/xml"},
]
feeds = adapter._select_feeds(rows)
assert feeds == ["https://itd.idaho.gov/wzdx.geojson", "https://alt.idaho/wzdx"]
def test_wanted_states_expands_abbrev_and_name(adapter):
"""'ID' matches both the abbreviation and 'idaho'."""
assert {"id", "idaho"} <= adapter._wanted_states()
def test_select_feeds_handles_garbage(adapter):
"""Non-list / non-dict rows never crash selection."""
assert adapter._select_feeds("not a list") == []
assert adapter._select_feeds([None, 3, {"state": "ID"}]) == [] # no url
def test_select_feeds_unwraps_socrata_url_object(adapter):
"""Live FHWA registry serializes Idaho's ``url`` as a Socrata "URL" column
object (``{"url": ""}``), not a bare string. Selection must unwrap it and
never raise ``AttributeError: 'dict' object has no attribute 'strip'``."""
rows = [
{"state": "Idaho", "format": "geojson",
"url": {"url": "https://511.idaho.gov/api/wzdx"}},
]
feeds = adapter._select_feeds(rows)
assert feeds == ["https://511.idaho.gov/api/wzdx"]
def test_tick_survives_socrata_url_object(adapter, monkeypatch):
"""End-to-end discovery path tolerates the nested-object ``url`` shape."""
registry = [{"state": "Idaho", "format": "geojson",
"url": {"url": "https://511.idaho.gov/api/wzdx"}}]
fc = make_feature_collection(make_wzdx_feature(feat_id="A"))
def fake_get(url, timeout=30):
return registry if "datahub" in url else fc
monkeypatch.setattr(adapter, "_http_get_json", fake_get)
# Must not raise; the unwrapped feed URL is discovered and fetched.
assert adapter.tick() is True
# Coalescing key (Part 1): wzdx_{road}|{lat:.3f}|{lon:.3f}|{sub_type} --
# no longer the raw data_source_id:feat_id pair, so feat_id="A" doesn't
# appear in the key.
assert {e["external_id"] for e in adapter.get_events()} == {
"wzdx_US-95|43.63|-116.915|lanes reduced, surface work"
}
# ============================================================
# CATEGORY + CANONICAL DATA
# ============================================================
def test_to_event_category_is_work_zone(adapter):
evt = adapter._parse_feature(make_wzdx_feature(), time.time())
event = adapter.to_event(evt)
assert event is not None
assert event.category == "work_zone"
def test_canonical_data_core_fields(adapter):
"""road / direction / mile posts / lat/lon map correctly."""
evt = adapter._parse_feature(make_wzdx_feature(), time.time())
d = adapter.to_event(evt).data
assert d["road"] == "US-95"
assert d["direction"] == "southbound" # full form the renderer prints
assert d["mile_start"] == 93
assert d["mile_end"] == 89
assert d["lat"] == pytest.approx(43.63, abs=0.05)
assert d["lon"] == pytest.approx(-116.915, abs=0.05)
def test_canonical_external_id_is_stable(adapter):
"""external_id is the Part-1 coalescing key: wzdx_{road}|{lat:.3f}|
{lon:.3f}|{sub_type} -- NOT the raw data_source_id:feature_id pair, so
multiple upstream features for the same physical zone collapse to one
traffic_events row / gating dedup key."""
evt = adapter._parse_feature(make_wzdx_feature(), time.time())
d = adapter.to_event(evt).data
assert d["external_id"] == "wzdx_US-95|43.63|-116.915|lanes reduced, surface work"
assert d["source"] == "wzdx"
def test_canonical_start_end_and_ends_epoch(adapter):
"""start_at / end_at epochs and ends_at_epoch are populated."""
evt = adapter._parse_feature(make_wzdx_feature(), time.time())
d = adapter.to_event(evt).data
# end_date 2026-07-19T00:59:59Z
expected_end = calendar.timegm((2026, 7, 19, 0, 59, 59, 0, 0, 0))
assert d["end_at"] == expected_end
assert d["start_at"] == calendar.timegm((2026, 7, 18, 13, 0, 0, 0, 0, 0))
assert d["ends_at_epoch"] == float(expected_end)
def test_canonical_lanes_and_subtype_partial(adapter):
"""some-lanes-closed → impact partial + folded sub_type + lanes phrase."""
evt = adapter._parse_feature(make_wzdx_feature(), time.time())
d = adapter.to_event(evt).data
assert d["impact"] == "partial"
assert d["sub_type"] == "lanes reduced, surface work"
assert d["lanes_affected"] == "lanes affected"
assert d["icon_category"] == "road_works"
def test_full_closure_maps_to_priority(adapter):
"""all-lanes-closed → priority severity + full_closure impact."""
feat = make_wzdx_feature(
vehicle_impact="all-lanes-closed", work_type="bridge-construction",
road="I-84", direction="eastbound",
)
evt = adapter._parse_feature(feat, time.time())
assert evt["severity"] == "priority"
event = adapter.to_event(evt)
assert event.severity == "priority"
assert event.data["impact"] == "full_closure"
assert event.data["lanes_affected"] == "all lanes closed"
# ============================================================
# FORMATTER INTEGRATION (real Phase-2 renderer)
# ============================================================
def test_renders_through_real_formatter(adapter):
"""Emitted data flows through formatters/incident.format() to a wire."""
now = time.time()
evt = adapter._parse_feature(make_wzdx_feature(), now)
event = adapter.to_event(evt)
wire = format_incident(event, now=now, budget=200)
assert wire.startswith("🚧")
assert "US-95" in wire
assert "mile 93" in wire
assert len(wire.encode("utf-8")) <= 80 # work-zone byte cap
def test_full_closure_wire_shows_all_lanes_closed(adapter):
feat = make_wzdx_feature(vehicle_impact="all-lanes-closed",
work_type="bridge-construction", road="I-84")
evt = adapter._parse_feature(feat, time.time())
wire = format_incident(adapter.to_event(evt), now=time.time(), budget=200)
assert "all lanes closed" in wire
# ============================================================
# FEED FETCH FLOW
# ============================================================
def test_tick_fetches_and_populates_events(adapter, monkeypatch):
"""A full tick discovers a feed, fetches it, and stores parsed events."""
registry = [{"state": "Idaho", "format": "geojson",
"url": "https://itd.idaho.gov/wzdx.geojson"}]
fc = make_feature_collection(
make_wzdx_feature(feat_id="A"),
make_wzdx_feature(feat_id="B", road="I-15", description="Road work"),
)
def fake_get(url, timeout=30):
return registry if "datahub" in url else fc
monkeypatch.setattr(adapter, "_http_get_json", fake_get)
changed = adapter.tick()
assert changed is True
events = adapter.get_events()
# Two DIFFERENT roads (US-95, I-15) -> 2 distinct coalescing keys (not
# over-collapsed); same lat/lon centroid + sub_type on both, road differs.
assert len(events) == 2
assert {e["external_id"] for e in events} == {
"wzdx_US-95|43.63|-116.915|lanes reduced, surface work",
"wzdx_I-15|43.63|-116.915|lanes reduced, surface work",
}
assert adapter.health_status["feed_count"] == 1
def test_feed_failure_does_not_crash(adapter, monkeypatch):
"""A down feed is skipped; the adapter survives and stays healthy-ish."""
registry = [{"state": "Idaho", "format": "geojson", "url": "https://itd/wzdx"}]
def fake_get(url, timeout=30):
if "datahub" in url:
return registry
raise TimeoutError("feed down")
monkeypatch.setattr(adapter, "_http_get_json", fake_get)
# Should not raise; no events, last_error recorded.
assert adapter.tick() is False
assert adapter.get_events() == []
assert adapter.health_status["last_error"] is not None
# ============================================================
# DEFENSIVE
# ============================================================
def test_empty_feature_returns_none(adapter):
assert adapter._parse_feature({}, time.time()) is None
def test_non_workzone_feature_skipped(adapter):
feat = make_wzdx_feature(event_type="special-event")
assert adapter._parse_feature(feat, time.time()) is None
def test_missing_coords_returns_none(adapter):
feat = make_wzdx_feature(coordinates=[])
feat["properties"].pop("core_details", None) # also no lat/lon
feat["geometry"] = {"type": "LineString", "coordinates": []}
assert adapter._parse_feature(feat, time.time()) is None
def test_no_stable_id_returns_none(adapter):
feat = make_wzdx_feature(data_source_id=None)
feat.pop("id", None)
feat["properties"]["core_details"].pop("data_source_id", None)
assert adapter._parse_feature(feat, time.time()) is None
def test_to_event_corrupted_dict_returns_none(adapter):
assert adapter.to_event({"garbage": True}) is None
def test_to_event_missing_coords_returns_none(adapter):
evt = adapter._parse_feature(make_wzdx_feature(), time.time())
evt["lat"] = None
assert adapter.to_event(evt) is None