mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
165 lines
6 KiB
Python
165 lines
6 KiB
Python
|
|
"""Tests for Phase 2b coverage-bbox wiring: fires, firms, ducting.
|
||
|
|
|
||
|
|
Verifies that:
|
||
|
|
1. Each adapter respects the `coverage` kwarg — derived scope wins when
|
||
|
|
coverage is provided, config field is the fallback when None.
|
||
|
|
2. fires._build_query_params() switches to the ArcGIS envelope filter in
|
||
|
|
coverage mode and drops the attr_POOState WHERE clause.
|
||
|
|
3. fires event_id backward-compat: an Idaho fire (attr_POOState="US-ID") in
|
||
|
|
coverage mode produces the same event_id as the single-state path.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from unittest.mock import MagicMock
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from meshai.coverage import resolve_adapter_coverage
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Shared reference bbox (Magic Valley / south-central Idaho)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
IDAHO_BOX = [-116.5, 42.0, -112.0, 44.0]
|
||
|
|
|
||
|
|
|
||
|
|
def _cov(adapter_name: str, bbox: list = None) -> dict | None:
|
||
|
|
return resolve_adapter_coverage(adapter_name, bbox or IDAHO_BOX, "native")
|
||
|
|
|
||
|
|
|
||
|
|
# ===========================================================================
|
||
|
|
# ducting
|
||
|
|
# ===========================================================================
|
||
|
|
|
||
|
|
def _ducting_cfg(lat=42.56, lon=-114.47):
|
||
|
|
cfg = MagicMock()
|
||
|
|
cfg.latitude = lat
|
||
|
|
cfg.longitude = lon
|
||
|
|
cfg.tick_seconds = 10800
|
||
|
|
return cfg
|
||
|
|
|
||
|
|
|
||
|
|
def test_ducting_coverage_wins():
|
||
|
|
"""When coverage is provided, lat/lon come from coverage['centroid']."""
|
||
|
|
from meshai.env.ducting import DuctingAdapter
|
||
|
|
cov = {"centroid": (43.0, -114.0)}
|
||
|
|
cfg = _ducting_cfg(lat=42.56, lon=-114.47)
|
||
|
|
adapter = DuctingAdapter(cfg, coverage=cov)
|
||
|
|
assert adapter._lat == 43.0
|
||
|
|
assert adapter._lon == -114.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_ducting_fallback_to_config():
|
||
|
|
"""When coverage=None, lat/lon fall back to config."""
|
||
|
|
from meshai.env.ducting import DuctingAdapter
|
||
|
|
cfg = _ducting_cfg(lat=42.56, lon=-114.47)
|
||
|
|
adapter = DuctingAdapter(cfg, coverage=None)
|
||
|
|
assert adapter._lat == cfg.latitude
|
||
|
|
assert adapter._lon == cfg.longitude
|
||
|
|
|
||
|
|
|
||
|
|
# ===========================================================================
|
||
|
|
# firms
|
||
|
|
# ===========================================================================
|
||
|
|
|
||
|
|
def _firms_cfg(bbox=None):
|
||
|
|
cfg = MagicMock()
|
||
|
|
cfg.map_key = "test-key"
|
||
|
|
cfg.source = "VIIRS_SNPP_NRT"
|
||
|
|
cfg.bbox = bbox if bbox is not None else [-117, 42, -114, 44]
|
||
|
|
cfg.day_range = 1
|
||
|
|
cfg.tick_seconds = 1800
|
||
|
|
cfg.confidence_min = "nominal"
|
||
|
|
cfg.proximity_km = 10.0
|
||
|
|
return cfg
|
||
|
|
|
||
|
|
|
||
|
|
def test_firms_coverage_wins():
|
||
|
|
"""When coverage is provided, adapter._bbox comes from coverage['bbox']."""
|
||
|
|
from meshai.env.firms import FIRMSAdapter
|
||
|
|
cov = {"bbox": [-116.5, 42.0, -112.0, 44.0]}
|
||
|
|
cfg = _firms_cfg(bbox=[-117, 42, -114, 44])
|
||
|
|
adapter = FIRMSAdapter(cfg, region_anchors=[], fires_adapter=None, coverage=cov)
|
||
|
|
assert adapter._bbox == [-116.5, 42.0, -112.0, 44.0]
|
||
|
|
|
||
|
|
|
||
|
|
def test_firms_fallback_to_config():
|
||
|
|
"""When coverage=None, adapter._bbox falls back to config.bbox."""
|
||
|
|
from meshai.env.firms import FIRMSAdapter
|
||
|
|
cfg = _firms_cfg(bbox=[-117, 42, -114, 44])
|
||
|
|
adapter = FIRMSAdapter(cfg, region_anchors=[], fires_adapter=None, coverage=None)
|
||
|
|
assert adapter._bbox == cfg.bbox
|
||
|
|
|
||
|
|
|
||
|
|
# ===========================================================================
|
||
|
|
# fires (NICFFiresAdapter)
|
||
|
|
# ===========================================================================
|
||
|
|
|
||
|
|
def _fires_cfg(state="US-ID"):
|
||
|
|
cfg = MagicMock()
|
||
|
|
cfg.state = state
|
||
|
|
cfg.tick_seconds = 600
|
||
|
|
return cfg
|
||
|
|
|
||
|
|
|
||
|
|
def test_fires_coverage_query_uses_envelope():
|
||
|
|
"""In coverage mode, _build_query_params includes envelope keys and drops attr_POOState WHERE."""
|
||
|
|
from meshai.env.fires import NICFFiresAdapter
|
||
|
|
cov = _cov("fires") # resolve via coverage module
|
||
|
|
assert cov is not None
|
||
|
|
assert "envelope" in cov
|
||
|
|
cfg = _fires_cfg()
|
||
|
|
adapter = NICFFiresAdapter(cfg, coverage=cov)
|
||
|
|
params = adapter._build_query_params()
|
||
|
|
# WHERE must not contain attr_POOState — spatial envelope scopes instead
|
||
|
|
assert "attr_POOState" not in params["where"]
|
||
|
|
assert "attr_IncidentTypeCategory='WF'" in params["where"]
|
||
|
|
# ArcGIS envelope keys must be present
|
||
|
|
assert "geometry" in params
|
||
|
|
assert params.get("geometryType") == "esriGeometryEnvelope"
|
||
|
|
assert params.get("spatialRel") == "esriSpatialRelIntersects"
|
||
|
|
assert params.get("inSR") == "4326"
|
||
|
|
|
||
|
|
|
||
|
|
def test_fires_no_coverage_query_uses_state_where():
|
||
|
|
"""Without coverage, _build_query_params retains the single-state WHERE clause."""
|
||
|
|
from meshai.env.fires import NICFFiresAdapter
|
||
|
|
cfg = _fires_cfg(state="US-ID")
|
||
|
|
adapter = NICFFiresAdapter(cfg, coverage=None)
|
||
|
|
params = adapter._build_query_params()
|
||
|
|
assert "attr_POOState='US-ID'" in params["where"]
|
||
|
|
assert "attr_IncidentTypeCategory='WF'" in params["where"]
|
||
|
|
# No envelope keys
|
||
|
|
assert "geometry" not in params
|
||
|
|
assert "geometryType" not in params
|
||
|
|
|
||
|
|
|
||
|
|
def test_fires_event_id_backward_compat():
|
||
|
|
"""Idaho fire in coverage mode yields the same event_id as the state path.
|
||
|
|
|
||
|
|
This ensures no re-broadcast of fires already known to the store when
|
||
|
|
the operator enables the coverage bbox.
|
||
|
|
"""
|
||
|
|
from meshai.env.fires import NICFFiresAdapter
|
||
|
|
|
||
|
|
fire_name = "Ross Fork Fire"
|
||
|
|
# Normalise the name the same way fires.py does
|
||
|
|
name_slug = fire_name.replace(" ", "_").lower()
|
||
|
|
|
||
|
|
# --- State path (coverage=None) ---
|
||
|
|
cfg = _fires_cfg(state="US-ID")
|
||
|
|
adapter_state = NICFFiresAdapter(cfg, coverage=None)
|
||
|
|
# Simulate what _fetch does for the event_id (coverage=None branch)
|
||
|
|
state_event_id = f"nifc_{name_slug}_{adapter_state._state}"
|
||
|
|
|
||
|
|
# --- Coverage path ---
|
||
|
|
cov = _cov("fires")
|
||
|
|
adapter_cov = NICFFiresAdapter(cfg, coverage=cov)
|
||
|
|
# Synthetic Idaho feature: attr_POOState matches config.state exactly
|
||
|
|
props = {"attr_POOState": "US-ID"}
|
||
|
|
per_fire_state = (props.get("attr_POOState") or "").strip() or adapter_cov._state
|
||
|
|
coverage_event_id = f"nifc_{name_slug}_{per_fire_state}"
|
||
|
|
|
||
|
|
assert coverage_event_id == state_event_id, (
|
||
|
|
f"event_id mismatch: coverage={coverage_event_id!r}, state={state_event_id!r}"
|
||
|
|
)
|