mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
_TOWN_ANCHORS_SEED covered only 29 hand-picked towns, while the live CT108 town_anchors table (GUI-curated since) has grown to 186. Any fresh deploy (fresh volume, or the pre-v19 sqlite path) would seed only the original 29 and silently lose anchor-resolution coverage for the other 157 real, already-in-production towns. Regenerated the dict from a live dump of CT108's town_anchors (name, lat, lon, state; all 186 rows confirmed enabled=1), alphabetized, same dict-of-dicts shape and column-aligned brace formatting as before (alignment column widened to fit the longest name, "mountain home afb"). seed_town_anchors() is unchanged and still INSERT OR IGNORE, so it stays idempotent against the existing production rows. Expanding the seed changes which town resolves as "nearest" for a few existing test fixtures whose incident/work-zone coordinates are genuinely closer to a newly-added real town (e.g. Oakley, Wilder) than to the old 29-town subset's nearest match -- those goldens and assertions are updated to the new (correct) nearest-town result. test_api_post_add_town's probe town is renamed from the now-real "Bellevue" to a fictitious "Testopolis" to avoid a duplicate-name 400.
455 lines
18 KiB
Python
455 lines
18 KiB
Python
"""Tests for env/fire_render.py's shared WFIGS wire-render + anchor helpers.
|
|
|
|
chore/ripout-2dii: `handle_wfigs` (the dead Central NATS-envelope entrypoint)
|
|
has been REMOVED from `meshai.env.fire_render` -- it had zero live production
|
|
callers (Central's consumer that drove it is gone). Tests that ONLY exercised
|
|
`handle_wfigs`'s own envelope-parsing / New-Update-cooldown decision / audit-row
|
|
contract (with no live equivalent -- that decision now lives in the LIVE
|
|
`gating.fire.decide`, already covered end-to-end via the real native adapter in
|
|
`tests/test_fire_native_growth.py`) were deleted alongside it.
|
|
|
|
What remains here:
|
|
(k) location anchor priority: geocoder.city > nearest_town > landclass > county
|
|
-- `_location_anchor` is shared, LIVE code (used by `_render`, which is
|
|
called directly on the FIRMS `wildfire_growth` path). Rewritten to call
|
|
`_render` directly instead of routing through the dead handler.
|
|
- "size unknown" / "containment unknown" missing-acres rendering -- same
|
|
live-`_render` rationale, rewritten to call `_render` directly.
|
|
- budget-fit worst case / date-only discovery -- already called `_render`
|
|
directly (never used handle_wfigs); unchanged.
|
|
|
|
The envelope builders (`_make_active_envelope`, `_make_tombstone`,
|
|
`_make_perimeter`, `_normalize_wfigs`) are KEPT: they are shared fixture
|
|
infrastructure imported by other test files (test_fire_refactor.py,
|
|
test_fire_tracker_phase1.py, test_fire_age_gate.py) to build canonical dicts
|
|
for the LIVE `gating.fire.decide` / `notifications.formatters.fire.format`.
|
|
`_normalize_wfigs` is a verbatim-logic local replica of the WFIGS dispatch
|
|
branch of the deleted Central-envelope adapter-normalizer module's
|
|
`normalize()` + `_parse_wfigs_incidents` helper (that module is already gone
|
|
from production; this replica exists purely so fixture-building has no
|
|
dependency on it).
|
|
"""
|
|
|
|
import time
|
|
from typing import Any, Optional
|
|
|
|
import pytest
|
|
|
|
from meshai.env.fire_render import (
|
|
_render as _wfigs_render,
|
|
)
|
|
from meshai.persistence import close_thread_connection, init_db
|
|
from meshai.persistence import db as persistence_db
|
|
|
|
|
|
# ---------- local replica of the deleted normalize()/_parse_wfigs_incidents
|
|
# ---------- WFIGS dispatch (see module docstring) --------------------------
|
|
|
|
_WFIGS_ACRES_KEYS = ("DailyAcres", "IncidentSize")
|
|
_WFIGS_ACRES_RAW_KEYS = ("IncidentSize", "DiscoveryAcres", "FinalAcres")
|
|
_WFIGS_CONTAINED_KEYS = ("PercentContained",)
|
|
_WFIGS_CONTAINED_RAW_KEYS = ("PercentContained",)
|
|
|
|
|
|
def _first_non_null(d: dict, keys) -> Any:
|
|
for k in keys:
|
|
v = d.get(k)
|
|
if v is not None and v != "":
|
|
return v
|
|
return None
|
|
|
|
|
|
def _parse_wfigs_acres(inner_data: dict) -> Optional[float]:
|
|
val = _first_non_null(inner_data, _WFIGS_ACRES_KEYS)
|
|
if val is None:
|
|
raw = inner_data.get("raw") or {}
|
|
if isinstance(raw, dict):
|
|
val = _first_non_null(raw, _WFIGS_ACRES_RAW_KEYS)
|
|
if val is None:
|
|
return None
|
|
try: return float(val)
|
|
except (TypeError, ValueError): return None
|
|
|
|
|
|
def _parse_wfigs_contained(inner_data: dict) -> Optional[int]:
|
|
val = _first_non_null(inner_data, _WFIGS_CONTAINED_KEYS)
|
|
if val is None:
|
|
raw = inner_data.get("raw") or {}
|
|
if isinstance(raw, dict):
|
|
val = _first_non_null(raw, _WFIGS_CONTAINED_RAW_KEYS)
|
|
if val is None:
|
|
return None
|
|
try: return int(round(float(val)))
|
|
except (TypeError, ValueError): return None
|
|
|
|
|
|
def _parse_wfigs_incidents(inner_data: dict, geo: dict) -> Optional[dict]:
|
|
geocoder = geo.get("geocoder") or {}
|
|
irwin_id = inner_data.get("IrwinID") or inner_data.get("irwin_id")
|
|
name = inner_data.get("IncidentName")
|
|
itype = inner_data.get("IncidentTypeCategory")
|
|
if itype is not None and itype not in ("WF", "wildfire"):
|
|
return None
|
|
lat = inner_data.get("latitude")
|
|
lon = inner_data.get("longitude")
|
|
county = inner_data.get("POOCounty")
|
|
state = inner_data.get("POOState")
|
|
landclass = geocoder.get("landclass")
|
|
|
|
declared_at_epoch = None
|
|
fdt = inner_data.get("FireDiscoveryDateTime")
|
|
if isinstance(fdt, (int, float)):
|
|
declared_at_epoch = int(fdt / 1000) if fdt > 1e12 else int(fdt)
|
|
|
|
acres = _parse_wfigs_acres(inner_data)
|
|
contained_pct = _parse_wfigs_contained(inner_data)
|
|
city = geocoder.get("city")
|
|
raw = inner_data.get("raw") or {}
|
|
|
|
return {
|
|
"irwin_id": irwin_id,
|
|
"incident_name": name,
|
|
"incident_type": itype,
|
|
"acres": acres,
|
|
"contained_pct": contained_pct,
|
|
"lat": lat,
|
|
"lon": lon,
|
|
"county": county,
|
|
"state": state,
|
|
"landclass": landclass,
|
|
"geocoder_city": city,
|
|
"declared_at_epoch": declared_at_epoch,
|
|
"fire_cause": raw.get("FireCause"),
|
|
"agency": raw.get("POOJurisdictionalAgency"),
|
|
"personnel": raw.get("TotalIncidentPersonnel"),
|
|
"unique_fire_id": raw.get("UniqueFireIdentifier"),
|
|
}
|
|
|
|
|
|
def _normalize_wfigs(envelope: dict) -> Optional[dict]:
|
|
"""wfigs_incidents/wfigs_perimeters envelope -> flat normalized dict,
|
|
same shape the deleted normalizer's normalize() used to produce."""
|
|
inner = envelope.get("data") or {}
|
|
adapter = inner.get("adapter") or ""
|
|
inner_data = inner.get("data") or {}
|
|
geo = inner.get("geo") or {}
|
|
category_raw = inner.get("category") or ""
|
|
|
|
if adapter == "wfigs_incidents":
|
|
if category_raw.startswith("fire.incident.removed"):
|
|
return {
|
|
"_kind": "wfigs_tombstone",
|
|
"irwin_id": inner_data.get("irwin_id") or inner_data.get("IrwinID"),
|
|
"state": inner_data.get("state") or inner_data.get("POOState"),
|
|
"county": inner_data.get("county") or inner_data.get("POOCounty"),
|
|
}
|
|
if category_raw.startswith("fire.incident"):
|
|
n = _parse_wfigs_incidents(inner_data, geo)
|
|
if n is None:
|
|
return None
|
|
n["_kind"] = "wfigs_incident"
|
|
return n
|
|
if adapter == "wfigs_perimeters":
|
|
return {
|
|
"_kind": "wfigs_perimeter",
|
|
"irwin_id": inner_data.get("irwin_id") or inner_data.get("IrwinID"),
|
|
"state": inner_data.get("state") or inner_data.get("POOState"),
|
|
"county": inner_data.get("county") or inner_data.get("POOCounty"),
|
|
}
|
|
return None
|
|
|
|
|
|
# ---------- fixtures ------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def mem_db(monkeypatch, tmp_path):
|
|
"""Fresh on-disk SQLite per test (avoids in-memory shared-cache bleed)."""
|
|
db_path = str(tmp_path / "wfigs-test.sqlite")
|
|
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
|
|
persistence_db._initialised.clear()
|
|
close_thread_connection()
|
|
conn = init_db()
|
|
try:
|
|
from meshai.adapter_config import adapter_config as _ac
|
|
_ac.invalidate()
|
|
except Exception:
|
|
pass
|
|
# Reset the stale-fire cleanup throttle so it runs deterministically.
|
|
try:
|
|
from meshai.env import fire_render as _wh
|
|
_wh._last_cleanup = 0
|
|
except Exception:
|
|
pass
|
|
yield conn
|
|
close_thread_connection()
|
|
persistence_db._initialised.discard(db_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def no_photon(monkeypatch):
|
|
"""Force nearest_town to return None so anchor falls through deterministically.
|
|
Tests that exercise nearest_town wire it in directly."""
|
|
from meshai import geo
|
|
monkeypatch.setattr(geo, "_photon_reverse_places", lambda lat, lon: [])
|
|
# Also reset the H3 LRU so cache state doesn't leak across tests.
|
|
if hasattr(geo, "_H3_NEAREST_CACHE"):
|
|
geo._H3_NEAREST_CACHE.clear()
|
|
|
|
|
|
# ---------- envelope builders --------------------------------------------
|
|
|
|
|
|
_IRWIN_A = "{E7FCBC00-2D0A-49D6-889F-550D4EDCBFD6}"
|
|
_IRWIN_B = "{ABCDEF01-2345-6789-ABCD-EF0123456789}"
|
|
_IRWIN_C = "{11111111-2222-3333-4444-555555555555}"
|
|
|
|
|
|
def _make_active_envelope(*, irwin_id=_IRWIN_A,
|
|
name="Cache Peak Fire",
|
|
incident_type="wildfire",
|
|
lat=42.197, lon=-113.710,
|
|
county="Cassia", state="ID",
|
|
landclass=None,
|
|
geocoder_city=None,
|
|
daily_acres=1847.0,
|
|
pct_contained=23,
|
|
raw_discovery_acres=None,
|
|
raw_pct_contained=None,
|
|
fire_discovery_dt_ms=1780529163000,
|
|
subject="central.fire.incident.id.cassia"):
|
|
"""Build the Central CloudEvents envelope shape we observe in prod."""
|
|
geocoder = {}
|
|
if geocoder_city is not None:
|
|
geocoder["city"] = geocoder_city
|
|
if landclass is not None:
|
|
geocoder["landclass"] = landclass
|
|
raw = {}
|
|
if raw_discovery_acres is not None:
|
|
raw["DiscoveryAcres"] = raw_discovery_acres
|
|
if raw_pct_contained is not None:
|
|
raw["PercentContained"] = raw_pct_contained
|
|
return {
|
|
"subject": subject,
|
|
"id": f"{irwin_id}:active:{int(time.time())}",
|
|
"data": {
|
|
"id": irwin_id,
|
|
"adapter": "wfigs_incidents",
|
|
"category": f"fire.incident.{incident_type}",
|
|
"severity": "routine",
|
|
"geo": {
|
|
"primary_region": f"US-{state}",
|
|
"centroid": [lon, lat],
|
|
"geocoder": geocoder,
|
|
},
|
|
"data": {
|
|
"IrwinID": irwin_id,
|
|
"IncidentName": name,
|
|
"IncidentTypeCategory": incident_type,
|
|
"latitude": lat,
|
|
"longitude": lon,
|
|
"POOCounty": county,
|
|
"POOState": state,
|
|
"DailyAcres": daily_acres,
|
|
"PercentContained": pct_contained,
|
|
"FireDiscoveryDateTime": fire_discovery_dt_ms,
|
|
"raw": raw,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _make_tombstone(irwin_id=_IRWIN_A, state="ID", county="Boise",
|
|
subject="central.fire.incident.removed.id"):
|
|
return {
|
|
"subject": subject,
|
|
"id": f"{irwin_id}:removed:2026-06-04T02:57:04.684858+00:00",
|
|
"data": {
|
|
"id": f"{irwin_id}:removed:2026-06-04T02:57:04.684858+00:00",
|
|
"adapter": "wfigs_incidents",
|
|
"category": "fire.incident.removed",
|
|
"severity": "routine",
|
|
"geo": {"primary_region": f"US-{state}", "geocoder": {}},
|
|
"data": {
|
|
"irwin_id": irwin_id,
|
|
"last_observed_at": "2026-06-04T02:52:04.209539+00:00",
|
|
"state": state,
|
|
"county": county,
|
|
"reason": "fallen_off_current_service",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _make_perimeter(irwin_id=_IRWIN_A, state="ID", county="Cassia",
|
|
subject="central.fire.perimeter.id.cassia"):
|
|
return {
|
|
"subject": subject,
|
|
"id": f"{irwin_id}:perimeter",
|
|
"data": {
|
|
"id": f"{irwin_id}:perimeter",
|
|
"adapter": "wfigs_perimeters",
|
|
"category": "fire.perimeter.wildfire",
|
|
"severity": "routine",
|
|
"geo": {"primary_region": f"US-{state}", "geocoder": {}},
|
|
"data": {
|
|
"irwin_id": irwin_id,
|
|
"state": state,
|
|
"county": county,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Missing-acres rendering -- LIVE `_render` behavior, called directly
|
|
# (formerly driven through the dead handle_wfigs).
|
|
# ============================================================================
|
|
|
|
|
|
def test_acres_missing_renders_na(mem_db, no_photon):
|
|
env = _make_active_envelope(name="IA 7", daily_acres=None,
|
|
pct_contained=None,
|
|
irwin_id=_IRWIN_C,
|
|
landclass="Sawtooth National Forest")
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
assert wire is not None
|
|
assert "size unknown" in wire
|
|
assert "containment unknown" in wire
|
|
|
|
|
|
def test_ia_placeholder_passthrough(mem_db, no_photon):
|
|
env = _make_active_envelope(name="IA 1", county="Elmore",
|
|
daily_acres=None, pct_contained=None,
|
|
landclass="Sawtooth National Forest",
|
|
irwin_id=_IRWIN_B)
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
assert wire is not None
|
|
assert "IA 1" in wire
|
|
|
|
|
|
# ============================================================================
|
|
# location anchor priority -- city > nearest_town > landclass > county
|
|
# `_location_anchor` is shared LIVE code (used by `_render`).
|
|
# ============================================================================
|
|
def test_k_anchor_geocoder_city_wins(mem_db, no_photon):
|
|
env = _make_active_envelope(geocoder_city="Twin Falls",
|
|
landclass="Sawtooth NF",
|
|
county="Cassia")
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
assert "Twin Falls" in wire
|
|
assert "Sawtooth NF" not in wire
|
|
assert "Cassia Co" not in wire
|
|
|
|
|
|
def test_k_anchor_falls_to_nearest_town(monkeypatch, mem_db):
|
|
"""When city missing, nearest_town(distance, bearing) feeds the anchor."""
|
|
fake = {"name": "Boise", "distance_mi": 47.0, "bearing": "S"}
|
|
monkeypatch.setattr(
|
|
"meshai.geo.nearest_town",
|
|
lambda lat, lon, max_distance_mi=50.0: fake,
|
|
)
|
|
env = _make_active_envelope(geocoder_city=None,
|
|
landclass="Sawtooth NF",
|
|
county="Cassia")
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
# Resolves anchor via town_anchors table (Oakley @ 42.24206, -113.883058
|
|
# -- now the nearest seeded anchor to the incident's 42.197,-113.710
|
|
# after the full-list seed sync; Burley is farther away)
|
|
assert "Oakley" in wire
|
|
|
|
|
|
def test_k_anchor_falls_to_landclass(monkeypatch, mem_db):
|
|
monkeypatch.setattr(
|
|
"meshai.geo.nearest_town",
|
|
lambda lat, lon, max_distance_mi=50.0: None,
|
|
)
|
|
env = _make_active_envelope(geocoder_city=None,
|
|
landclass="Sawtooth National Forest",
|
|
county="Cassia")
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
# Resolves nearest town from town_anchors table, overriding landclass
|
|
# (Oakley is nearest to 42.197,-113.710 after the full-list seed sync)
|
|
assert "Oakley" in wire
|
|
|
|
|
|
def test_k_anchor_falls_to_county(monkeypatch, mem_db):
|
|
monkeypatch.setattr(
|
|
"meshai.geo.nearest_town",
|
|
lambda lat, lon, max_distance_mi=50.0: None,
|
|
)
|
|
env = _make_active_envelope(geocoder_city=None, landclass=None,
|
|
county="Cassia", state="ID")
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
# Resolves nearest town from town_anchors table
|
|
# (Oakley is nearest to 42.197,-113.710 after the full-list seed sync)
|
|
assert "Oakley" in wire
|
|
|
|
|
|
def test_k_anchor_nearest_town_under_one_mile_says_near(monkeypatch, mem_db):
|
|
fake = {"name": "Burley", "distance_mi": 0.3, "bearing": "N"}
|
|
monkeypatch.setattr(
|
|
"meshai.geo.nearest_town",
|
|
lambda lat, lon, max_distance_mi=50.0: fake,
|
|
)
|
|
env = _make_active_envelope(geocoder_city=None)
|
|
n = _normalize_wfigs(env)
|
|
wire = _wfigs_render(n, prefix="New")
|
|
# Anchor resolved via town_anchors; exact format depends on distance
|
|
# (Oakley is nearest to 42.197,-113.710 after the full-list seed sync)
|
|
assert "Oakley" in wire
|
|
|
|
|
|
# ============================================================================
|
|
# Budget-fit worst case: longest plausible fire payload fits 140 chars, with
|
|
# no `ID:` line, no `**` markdown, and discovery rendered DATE-ONLY.
|
|
# (LIVE `_render`, called directly -- never used handle_wfigs.)
|
|
# ============================================================================
|
|
|
|
|
|
def test_wfigs_worst_case_fits_140():
|
|
n = {
|
|
"incident_name": "East Fork Salmon River Complex Lightning Fire",
|
|
"acres": 128456,
|
|
"contained_pct": 42,
|
|
"fire_cause": "Lightning",
|
|
"unique_fire_id": "2026-IDSCF-000987",
|
|
# Jun 18 2026 ~14:30 local
|
|
"declared_at_epoch": 1_781_204_400,
|
|
"geocoder_city": "Near Clayton, ID",
|
|
}
|
|
wire = _wfigs_render(n, prefix="Update", last_bcast_acres=100000)
|
|
|
|
assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
|
|
# critical fields
|
|
assert "East Fork Salmon River Complex Lightning Fire" in wire # name
|
|
assert "128,456 ac" in wire # acreage
|
|
assert "containment 42%" in wire # containment
|
|
assert "Near Clayton, ID" in wire # location
|
|
assert "Cause: Lightning" in wire # cause
|
|
# format rules
|
|
assert "ID:" not in wire, "unique-fire-id line must be dropped"
|
|
assert "**" not in wire, "no bold markdown"
|
|
|
|
|
|
def test_wfigs_discovery_is_date_only():
|
|
n = {
|
|
"incident_name": "Short Fire",
|
|
"acres": 100,
|
|
"contained_pct": 0,
|
|
"fire_cause": "Human",
|
|
"unique_fire_id": "2026-X",
|
|
"declared_at_epoch": 1_781_204_400, # renders Jun 11 in the handler's UTC-6
|
|
"geocoder_city": "Boise",
|
|
}
|
|
wire = _wfigs_render(n, prefix="New")
|
|
assert "Discovered Jun 11" in wire
|
|
# no time-of-day (colon in an H:MM would appear as ":3" etc.)
|
|
assert "2:30" not in wire and "PM" not in wire and "AM" not in wire
|
|
assert "ID:" not in wire
|