"""Persistence tests for the dedicated native WZDx ingest path. The daily summary (wzdx_summary.fire_once) and DM detail (env_reporter.build_work_zones_detail) both read the CURRENT active work-zone set straight from traffic_events: SELECT ... FROM traffic_events WHERE source='wzdx' AND (end_at IS NULL OR end_at >= now) Before the fix, native wzdx rode the generic ``_delta_emit`` path, which silent-seeds every zone on the cold-start first poll and ``return``s BEFORE the incident decider's ``INSERT INTO traffic_events`` runs -- so the current coalesced set never persisted and the summary counted ~0. ``store._ingest_wzdx`` now UPSERTS the current coalesced set into traffic_events every poll (persist-only, never emit/decide/broadcast) and reconciles removals, so the table always equals the current active set the summary reads. These tests drive the REAL EnvironmentalStore + EventBus with a fake WZDx adapter whose per-poll coalesced set we control, then assert directly against traffic_events AND against the bus (nothing must ever be dispatched). """ from __future__ import annotations import asyncio from meshai.env.store import EnvironmentalStore from meshai.config import EnvironmentalConfig from meshai.notifications.pipeline.bus import EventBus from meshai.notifications.events import make_event from meshai.persistence import get_db class _FakeWZDx: """Native-WZDx stand-in whose coalesced current set the test controls. Its stored-event dicts carry the SAME shape env/wzdx.py emits: a coalesced ``external_id`` (``wzdx_|||``), ``lat``/``lon``, ``start_at``/``end_at`` epochs, and a ``normalized`` dict (the _parse_wzdx_federal output: road/direction/mile_start/mile_end/sub_type/ impact). ``get_events()`` returns exactly the current coalesced set -- which is what ``_ingest_wzdx`` upserts + reconciles against. """ def __init__(self): self._batch: list[dict] = [] self.emitted = False # set True if to_event() is ever called def set_zones(self, zones: list[dict]) -> None: """zones: list of {ext, road, lat, lon, sub_type, impact, end_at, start_at?, direction?} -> stored-event dicts like env/wzdx builds.""" batch = [] for z in zones: ext = z["ext"] n = { "road": z.get("road"), "direction": z.get("direction"), "mile_start": z.get("mile_start"), "mile_end": z.get("mile_end"), "sub_type": z.get("sub_type"), "impact": z.get("impact"), } batch.append({ "source": "wzdx", "event_id": ext, "external_id": ext, "lat": z.get("lat"), "lon": z.get("lon"), "start_at": z.get("start_at"), "end_at": z.get("end_at"), "severity": "priority" if z.get("impact") == "full_closure" else "routine", "normalized": n, "fetched_at": 0, }) self._batch = batch def set_raw(self, batch: list[dict]) -> None: """Set the raw stored-event batch verbatim (edge cases).""" self._batch = batch def tick(self) -> bool: # Real wzdx.tick() returns True even on a 0-event (registry-only) tick. return True def get_events(self) -> list: return list(self._batch) def to_event(self, raw_evt: dict): # If _ingest_wzdx ever emitted, it would call this. It must NOT. self.emitted = True return make_event(source="wzdx", category="work_zone", severity="routine", title=raw_evt.get("external_id"), summary=raw_evt.get("external_id"), group_key=raw_evt.get("external_id")) def _build_store(adapter): """Construct a store (runs the durable pre-seed against the fresh migrated DB the conftest points MESHAI_DB_PATH at), then inject the fake adapter. Captures everything that reaches the EventBus so we can assert silence.""" bus = EventBus() captured: list = [] bus.subscribe(lambda e: captured.append(e)) store = EnvironmentalStore(EnvironmentalConfig(), event_bus=bus) store._adapters["wzdx"] = adapter return store, captured def _wzdx_rows(): conn = get_db() return conn.execute( "SELECT external_id, source, road, sub_type, impact, lat, lon, " "end_at, first_seen_at, last_seen_at, last_broadcast_at " "FROM traffic_events WHERE source='wzdx' " "ORDER BY external_id" ).fetchall() def _summary_visible_count(now: int): """Count exactly what wzdx_summary.fire_once / build_work_zones_detail see: source='wzdx' AND not-expired.""" conn = get_db() return conn.execute( "SELECT COUNT(*) FROM traffic_events " "WHERE source='wzdx' AND (end_at IS NULL OR end_at >= ?)", (now,), ).fetchone()[0] ZONES3 = [ {"ext": "wzdx_US-20|43.600|-116.200|construction work", "road": "US-20", "lat": 43.6, "lon": -116.2, "sub_type": "construction work", "impact": "partial", "end_at": None}, {"ext": "wzdx_I-84|43.500|-116.400|construction work", "road": "I-84", "lat": 43.5, "lon": -116.4, "sub_type": "construction work", "impact": "full_closure", "end_at": 9_000_000_000}, {"ext": "wzdx_ID-55|44.000|-116.000|maintenance", "road": "ID-55", "lat": 44.0, "lon": -116.0, "sub_type": "maintenance", "impact": "partial", "end_at": 9_000_000_000}, ] def test_first_poll_persists_current_set_and_broadcasts_nothing(): # THE GAP FIX: the cold-start first poll must PERSIST every current # coalesced zone into traffic_events (source='wzdx') and emit NOTHING. adapter = _FakeWZDx() store, captured = _build_store(adapter) adapter.set_zones(ZONES3) asyncio.run(store.refresh()) # first (cold-start) poll rows = _wzdx_rows() exts = {r["external_id"] for r in rows} assert exts == {z["ext"] for z in ZONES3}, ( "first poll must persist ALL current coalesced zones (the gap fix)") # No broadcast / no emit occurred. assert captured == [], "persist-only: NOTHING may reach the pipeline bus" assert adapter.emitted is False, "to_event()/emit must never be called" # Every row is source='wzdx' with last_broadcast_at NULL (never armed). for r in rows: assert r["source"] == "wzdx" assert r["last_broadcast_at"] is None # The summary/DM query now sees the real count, not ~0. assert _summary_visible_count(now=0) == 3 def test_columns_match_summary_and_dm_queries(): # The persisted columns the summary (road/lat/lon/sub_type/impact/end_at) # and DM (road/direction/sub_type/impact/lat/lon/end_at) read must be # populated from the coalesced zone, not left NULL. adapter = _FakeWZDx() store, _ = _build_store(adapter) adapter.set_zones([ZONES3[1]]) # the full_closure I-84 zone asyncio.run(store.refresh()) r = _wzdx_rows()[0] assert r["road"] == "I-84" assert r["sub_type"] == "construction work" assert r["impact"] == "full_closure" assert abs(r["lat"] - 43.5) < 1e-9 assert abs(r["lon"] - (-116.4)) < 1e-9 assert r["end_at"] == 9_000_000_000 def test_subsequent_poll_reconciles_removed_zone(): # A zone that DROPS OUT of the feed on a later poll must be REMOVED so the # table equals the CURRENT active set (else stale rows inflate the count). adapter = _FakeWZDx() store, captured = _build_store(adapter) adapter.set_zones(ZONES3) asyncio.run(store.refresh()) assert len(_wzdx_rows()) == 3 # Next poll: US-20 dropped out; I-84 + ID-55 remain. adapter.set_zones([ZONES3[1], ZONES3[2]]) asyncio.run(store.refresh()) exts = {r["external_id"] for r in _wzdx_rows()} assert exts == {ZONES3[1]["ext"], ZONES3[2]["ext"]}, ( "the dropped zone must be reconciled out of traffic_events") assert captured == [], "reconcile is persist-only; still no broadcast" def test_empty_or_failed_fetch_does_not_wipe_existing_rows(): # An empty/failed fetch (get_events() == []) must NOT delete existing rows # -- a transient upstream outage must never zero out the summary's data. adapter = _FakeWZDx() store, _ = _build_store(adapter) adapter.set_zones(ZONES3) asyncio.run(store.refresh()) assert len(_wzdx_rows()) == 3 adapter.set_raw([]) # empty/failed poll asyncio.run(store.refresh()) assert len(_wzdx_rows()) == 3, ( "an empty fetch must NEVER wipe the existing active set") def test_upsert_preserves_first_seen_at_and_refreshes_end_at(): # A zone seen across polls keeps its first_seen_at but refreshes # last_seen_at + end_at (so the summary's not-expired filter tracks the # feed's latest end date). adapter = _FakeWZDx() store, _ = _build_store(adapter) z = dict(ZONES3[0]); z["end_at"] = 1000 adapter.set_zones([z]) asyncio.run(store.refresh()) r1 = _wzdx_rows()[0] first_seen = r1["first_seen_at"] assert r1["end_at"] == 1000 # Same zone reappears with a LATER end_at. z2 = dict(ZONES3[0]); z2["end_at"] = 5000 adapter.set_zones([z2]) asyncio.run(store.refresh()) r2 = _wzdx_rows()[0] assert r2["first_seen_at"] == first_seen, "first_seen_at must be preserved" assert r2["end_at"] == 5000, "end_at must refresh from the feed" def test_expiry_end_at_preserved_for_not_expired_filter(): # end_at must be persisted verbatim so the summary's not-expired filter # (end_at IS NULL OR end_at >= now) correctly includes/excludes zones. now = 1_000_000 adapter = _FakeWZDx() store, _ = _build_store(adapter) zones = [ {"ext": "wzdx_open|1.0|1.0|x", "road": "OPEN", "lat": 1.0, "lon": 1.0, "sub_type": "x", "impact": "partial", "end_at": None}, # open-ended {"ext": "wzdx_future|2.0|2.0|x", "road": "FUT", "lat": 2.0, "lon": 2.0, "sub_type": "x", "impact": "partial", "end_at": now + 10_000}, # not expired {"ext": "wzdx_past|3.0|3.0|x", "road": "PAST", "lat": 3.0, "lon": 3.0, "sub_type": "x", "impact": "partial", "end_at": now - 10_000}, # expired ] adapter.set_zones(zones) asyncio.run(store.refresh()) # All 3 persisted (ingest does not itself drop expired rows) ... assert len(_wzdx_rows()) == 3 # ... but the summary's not-expired filter counts only the open + future. assert _summary_visible_count(now=now) == 2 def test_id_less_zone_is_skipped_not_fatal(): # A stored event with no external_id cannot be keyed; it is skipped (never # persisted, never raises), and does not participate in reconciliation. adapter = _FakeWZDx() store, _ = _build_store(adapter) good = ZONES3[0] adapter.set_zones([good]) asyncio.run(store.refresh()) assert len(_wzdx_rows()) == 1 # Poll with the good zone plus an id-less junk event. adapter.set_zones([good]) junk = {"source": "wzdx", "event_id": None, "external_id": None, "lat": 5.0, "lon": 5.0, "normalized": {}, "fetched_at": 0} adapter._batch.append(junk) asyncio.run(store.refresh()) rows = _wzdx_rows() assert {r["external_id"] for r in rows} == {good["ext"]}, ( "id-less zone skipped; good zone still persisted, reconcile intact") def test_bulk_current_set_persists_all_like_the_real_127(): # Scale check mirroring the real ~127-zone active set: every coalesced # zone in a large current set persists and is visible to the summary. adapter = _FakeWZDx() store, captured = _build_store(adapter) zones = [ {"ext": f"wzdx_R{i}|{40.0 + i * 0.001:.3f}|-116.000|maintenance", "road": f"R{i}", "lat": 40.0 + i * 0.001, "lon": -116.0, "sub_type": "maintenance", "impact": "partial", "end_at": None} for i in range(127) ] adapter.set_zones(zones) asyncio.run(store.refresh()) assert len(_wzdx_rows()) == 127 assert _summary_visible_count(now=0) == 127, ( "the full active set is counted, not ~0") assert captured == [], "bulk persist still broadcasts nothing"