meshai/work/tests/test_wzdx_coalescing.py
malice 6050392d09
chore(central-ripout 2e): split central_normalizer.py; eliminate the "central" name (#165)
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: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:15:50 -06:00

179 lines
6.5 KiB
Python

"""Part 1 coalescing tests -- env/wzdx.py external_id merge.
Multiple WZDx features that resolve to the SAME coalescing key (same road +
rounded lat/lon + folded sub_type) must collapse to ONE merged stored event
(earliest start_at, latest end_at). Two features on DIFFERENT roads must
stay as 2 distinct keys (no over-collapsing).
"""
import time
from types import SimpleNamespace
import pytest
from meshai import geo
from meshai.env.wzdx import WZDxAdapter
@pytest.fixture(autouse=True)
def _no_photon(monkeypatch):
monkeypatch.setattr(geo, "nearest_town", lambda *a, **k: None)
@pytest.fixture
def mock_config():
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 _feature(feat_id, data_source_id, road, direction, start_date, end_date,
coordinates, description="Paving operations."):
"""Build a WZDx v4 GeoJSON road_event feature (US-26/Gooding-style fans:
same road/lat/lon/sub_type, different direction / feat_id / schedule
window)."""
core = {
"event_type": "work-zone",
"data_source_id": data_source_id,
"road_names": [road],
"direction": direction,
"description": description,
}
props = {
"core_details": core,
"types_of_work": [{"type_name": "surface-work"}],
"vehicle_impact": "some-lanes-closed",
"start_date": start_date,
"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)}
# Same road/coords (US-26 near Gooding) fanned across direction + feat_id +
# schedule-day window -- all 3 features describe the SAME physical zone.
_GOODING_COORDS = [[-114.71, 42.94], [-114.72, 42.93]]
def test_same_zone_fan_merges_to_one_event_earliest_start_latest_end(adapter):
"""Three US-26/Gooding-style features (same road+lat+lon+sub_type, fanned
across direction/feat_id/schedule-day) collapse to ONE merged event_id
with the earliest start_at and latest end_at across all three."""
feats = [
_feature("WZ-100", "idot-1", "US-26", "eastbound",
"2026-07-18T06:00:00Z", "2026-07-18T18:00:00Z", _GOODING_COORDS),
_feature("WZ-101", "idot-1", "US-26", "eastbound",
"2026-07-17T06:00:00Z", "2026-07-19T18:00:00Z", _GOODING_COORDS),
_feature("WZ-102", "idot-1", "US-26", "eastbound",
"2026-07-19T06:00:00Z", "2026-07-17T18:00:00Z", _GOODING_COORDS),
]
fc = make_feature_collection(*feats)
def fake_get(url, timeout=30):
return fc
now = time.time()
events = [adapter._parse_feature(f, now) for f in feats]
assert all(e is not None for e in events)
# All three parse to the SAME coalescing key (event_id == external_id).
keys = {e["event_id"] for e in events}
assert len(keys) == 1, f"expected 1 coalescing key, got {keys}"
merged = adapter._coalesce_events(events)
assert len(merged) == 1
m = merged[0]
# Earliest start_at across the three (2026-07-17T06:00:00Z is earliest).
import calendar
expected_start = calendar.timegm((2026, 7, 17, 6, 0, 0, 0, 0, 0))
expected_end = calendar.timegm((2026, 7, 19, 18, 0, 0, 0, 0, 0))
assert m["start_at"] == expected_start
assert m["end_at"] == expected_end
def test_different_roads_stay_distinct_keys(adapter):
"""Two features on DIFFERENT roads (same lat/lon/sub_type otherwise)
must NOT collapse -- 2 distinct coalescing keys survive coalescing."""
feat_a = _feature("WZ-200", "idot-1", "US-26", "eastbound",
"2026-07-18T06:00:00Z", "2026-07-18T18:00:00Z", _GOODING_COORDS)
feat_b = _feature("WZ-201", "idot-1", "I-84", "eastbound",
"2026-07-18T06:00:00Z", "2026-07-18T18:00:00Z", _GOODING_COORDS)
now = time.time()
ev_a = adapter._parse_feature(feat_a, now)
ev_b = adapter._parse_feature(feat_b, now)
assert ev_a is not None and ev_b is not None
assert ev_a["event_id"] != ev_b["event_id"]
merged = adapter._coalesce_events([ev_a, ev_b])
assert len(merged) == 2
assert {m["event_id"] for m in merged} == {ev_a["event_id"], ev_b["event_id"]}
def test_merge_keeps_none_end_at_when_either_side_open_ended(adapter):
"""If EITHER merged feature has no end_date, the merged end_at stays
None (an open-ended zone must never get a fabricated end)."""
feat_a = _feature("WZ-300", "idot-1", "US-26", "eastbound",
"2026-07-18T06:00:00Z", "2026-07-18T18:00:00Z", _GOODING_COORDS)
feat_b = dict(feat_a)
feat_b["id"] = "WZ-301"
feat_b["properties"] = dict(feat_a["properties"])
feat_b["properties"]["end_date"] = None
now = time.time()
ev_a = adapter._parse_feature(feat_a, now)
ev_b = adapter._parse_feature(feat_b, now)
assert ev_a["event_id"] == ev_b["event_id"]
assert ev_a["end_at"] is not None
assert ev_b["end_at"] is None
merged = adapter._coalesce_events([ev_a, ev_b])
assert len(merged) == 1
assert merged[0]["end_at"] is None
def test_fetch_all_end_to_end_coalesces_via_tick(adapter, monkeypatch):
"""End-to-end: adapter.tick() -> _fetch_all() coalesces a same-zone fan
down to one stored event via the real feed-fetch path."""
registry = [{"state": "Idaho", "format": "geojson",
"url": "https://itd.idaho.gov/wzdx.geojson"}]
feats = [
_feature("WZ-400", "idot-1", "US-26", "eastbound",
"2026-07-18T06:00:00Z", "2026-07-18T18:00:00Z", _GOODING_COORDS),
_feature("WZ-401", "idot-1", "US-26", "westbound",
"2026-07-17T06:00:00Z", "2026-07-19T18:00:00Z", _GOODING_COORDS),
]
fc = make_feature_collection(*feats)
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()
assert len(events) == 1
import calendar
assert events[0]["start_at"] == calendar.timegm((2026, 7, 17, 6, 0, 0, 0, 0, 0))
assert events[0]["end_at"] == calendar.timegm((2026, 7, 19, 18, 0, 0, 0, 0, 0))