From 2eddd9b572a116577282ee03eb9589107639920e Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sun, 16 Aug 2026 18:00:12 +0000 Subject: [PATCH] fix(reminders): resolve Active: fire location via _fire_anchor, not county/state ReminderScheduler._render built the wfigs "Active:" reminder location as a bare " / ".join(county, state), skipping the anchor resolution (geocoder_city -> curated town_anchors -> Photon -> landclass -> county -> state) that the New/Update fire formatter (formatters/fire.py::_fire_anchor) already uses. Reminders now call the same _fire_anchor helper on dict(row) (sqlite3.Row has no .get()) and fit the result to fit_to_budget(..., budget_for("wfigs")), matching how the other renderers guard the mesh packet budget. --- .../notifications/reminders/__init__.py | 8 +- work/tests/test_reminders.py | 91 +++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/work/meshai/notifications/reminders/__init__.py b/work/meshai/notifications/reminders/__init__.py index 30b6067..21c93a5 100644 --- a/work/meshai/notifications/reminders/__init__.py +++ b/work/meshai/notifications/reminders/__init__.py @@ -59,6 +59,9 @@ import time from datetime import datetime, timezone from typing import Any, Optional +from meshai.notifications.formatters._budget import budget_for, fit_to_budget +from meshai.notifications.formatters.fire import _fire_anchor + logger = logging.getLogger(__name__) @@ -370,8 +373,9 @@ class ReminderScheduler: cont = row["current_contained_pct"] acres_s = "N/A" if acres is None else f"{int(acres):,} ac" cont_s = "?" if cont is None else f"{int(cont)}%" - anchor = " / ".join(p for p in (row["county"], row["state"]) if p) or "?" - return f"🔥 {prefix}: {name}, {anchor}: {acres_s}, {cont_s} contained" + anchor = _fire_anchor(dict(row)) + wire = f"🔥 {prefix}: {name}, {anchor}: {acres_s}, {cont_s} contained" + return fit_to_budget(wire, budget_for("wfigs")) if adapter == "swpc": return f"🌌 {prefix}: ongoing space weather event ({row['event_type']})" if adapter == "itd_511_work_zone": diff --git a/work/tests/test_reminders.py b/work/tests/test_reminders.py index aaa2122..d572a89 100644 --- a/work/tests/test_reminders.py +++ b/work/tests/test_reminders.py @@ -229,3 +229,94 @@ def test_work_zone_reminder_skipped_outside_slot(mock_dispatcher): sch = ReminderScheduler(mock_dispatcher, clock=lambda: now, tick_seconds=60) fired = asyncio.run(sch.tick_once()) assert fired == 0 + + +# ============================================================================ +# Anchor resolution in the wfigs "Active:" reminder wire (_fire_anchor) +# ============================================================================ + + +def test_wfigs_reminder_uses_anchor_not_county(mock_dispatcher): + """A fire near a known curated town_anchors entry (Bliss, ID) renders the + anchor phrase ('mi of Bliss'), not the bare county/state join + the old renderer produced.""" + now = 1_780_000_000 + conn = get_db() + _enable_wfigs_reminders() + # Coordinates ~4 mi NW of Bliss, ID (see _TOWN_ANCHORS_SEED in + # meshai/persistence/curation.py) -- county/state deliberately different + # from "Bliss" so the assertion can't pass by accident via the county. + conn.execute( + "INSERT OR REPLACE INTO fires(irwin_id, incident_name, incident_type, " + "current_acres, current_contained_pct, lat, lon, county, state, " + "declared_at, last_event_at, first_broadcast_at, last_broadcast_at) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", + ("F-ANCHOR", "Jackalope", "WF", 15, 0, + 42.98, -114.99, "Gooding", "ID", + now - 9 * 3600, now, now - 9 * 3600, now - 9 * 3600), + ) + + sch = ReminderScheduler(mock_dispatcher, clock=lambda: now) + fired = asyncio.run(sch.tick_once()) + assert fired == 1 + args = mock_dispatcher.dispatch_scheduled_fire_broadcast.call_args.kwargs + text = args["text"] + assert "Bliss" in text + assert "of Bliss" in text + # Old format ("Gooding / ID") must be gone. + assert "Gooding / ID" not in text + + +def test_wfigs_reminder_falls_back_when_no_anchor_nearby(mock_dispatcher): + """A fire with no lat/lon at all still renders sensibly via + _fire_anchor's fallback chain (county -> state -> "location unknown"), + and never raises.""" + now = 1_780_000_000 + conn = get_db() + _enable_wfigs_reminders() + conn.execute( + "INSERT OR REPLACE INTO fires(irwin_id, incident_name, incident_type, " + "current_acres, current_contained_pct, lat, lon, county, state, " + "declared_at, last_event_at, first_broadcast_at, last_broadcast_at) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", + ("F-NOANCHOR", "Remote Fire", "WF", 42, 5, + None, None, "Owyhee", "ID", + now - 9 * 3600, now, now - 9 * 3600, now - 9 * 3600), + ) + + sch = ReminderScheduler(mock_dispatcher, clock=lambda: now) + fired = asyncio.run(sch.tick_once()) + assert fired == 1 + args = mock_dispatcher.dispatch_scheduled_fire_broadcast.call_args.kwargs + text = args["text"] + # Falls through landclass (absent) to the "{county} Co {state}" tier. + assert "Owyhee Co ID" in text + assert "Active" in text + + +def test_wfigs_reminder_truncates_to_budget(mock_dispatcher): + """A very long incident name + anchor is truncated to the wfigs budget + (fit_to_budget) rather than exceeding the mesh packet limit.""" + now = 1_780_000_000 + conn = get_db() + _enable_wfigs_reminders() + long_name = ("The Extremely Long Wildfire Incident Name That Keeps Going " + "And Going And Going Well Past Any Reasonable Mesh Packet Budget") + conn.execute( + "INSERT OR REPLACE INTO fires(irwin_id, incident_name, incident_type, " + "current_acres, current_contained_pct, lat, lon, county, state, " + "declared_at, last_event_at, first_broadcast_at, last_broadcast_at) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", + ("F-LONG", long_name, "WF", 123456, 3, + 42.98, -114.99, "Gooding", "ID", + now - 9 * 3600, now, now - 9 * 3600, now - 9 * 3600), + ) + + from meshai.notifications.formatters._budget import budget_for + sch = ReminderScheduler(mock_dispatcher, clock=lambda: now) + fired = asyncio.run(sch.tick_once()) + assert fired == 1 + args = mock_dispatcher.dispatch_scheduled_fire_broadcast.call_args.kwargs + text = args["text"] + assert len(text) <= budget_for("wfigs") + assert text.endswith("…")