mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 09:21:33 +00:00
- mesh_data_store.py / env/store.py: make refresh() async, offload blocking polls via asyncio.to_thread/gather so 7 lockstep sources no longer starve the shared event loop. - main.py: gather pollers concurrently + set_default_executor thread pool. - Dockerfile / docker-compose.yml: healthcheck now curls the dashboard for a real liveness signal instead of a process-exists check. - transport/meshcore_transport.py: MeshCore keepalive loop (get_time() every 120s), reconnect re-arm (_post_reconnect_setup_async from _on_connect_event), and MC channel-name normalization (_resolve_mc_channel_idx strips a leading #). - dashboard-frontend: MeshCoreConnection.tsx config-page hardening, new ErrorBoundary component, wired into App.tsx. - tests: fix ~40 call sites broken by refresh() becoming async ( test_generic_http.py, test_store_received_delta.py, test_store_wzdx_persist.py) by wrapping with asyncio.run(), matching this suite's existing convention for calling async code from sync test functions. Verified: all 40 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
305 lines
12 KiB
Python
305 lines
12 KiB
Python
"""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_<road>|<lat>|<lon>|<sub_type>``), ``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"
|