feat(broadcast): fit all mesh message formats to the packet budget (#8)

Tighten broadcast formats to fit 140 chars with critical info preserved:
traffic (directions/milepost never abbreviated, narrative trimmed from
end), nws hazard wording tightened (towns already path-sampled), fires
(drop ID line + ** + discovery time), avy (advice -> first sentence),
satpass/quake safety cap. Fire digest broadcast disabled by default.
All budget-aware via the shared max_chars.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-07-02 16:49:33 -06:00 committed by GitHub
commit 6bc57709a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 546 additions and 116 deletions

View file

@ -194,3 +194,49 @@ def test_missing_event_id_returns_none(adapter):
def test_does_not_raise_on_corrupted_dict(adapter):
"""Corrupted dict returns None without raising."""
assert adapter.to_event({"garbage": True}) is None
# ============================================================================
# Central avy_handler._render (mesh broadcast wire) -- budget-fit format.
# advice -> FIRST SENTENCE only; zone/level/source kept FULL; fits 140.
# ============================================================================
from meshai.central.avy_handler import _render as _avy_render
def test_avy_render_advice_first_sentence_only():
wire = _avy_render(
danger_level=4, danger_name="High",
zone_name="Western Mountains",
center_id="SNFAC",
travel=("Avoid all avalanche terrain today. Natural and human-triggered "
"avalanches are likely on steep slopes."),
)
# first sentence retained (with its period), the rest dropped
assert "Avoid all avalanche terrain today." in wire
assert "Natural and human-triggered" not in wire
# zone / level / source kept FULL (never abbreviated)
assert "Western Mountains" in wire
assert "High (4)" in wire
assert "SNFAC" in wire
def test_avy_render_worst_case_fits_140():
# Long multi-sentence advice paragraph -- only the first sentence is kept,
# which lets the full zone / level / source survive under the 140 budget.
wire = _avy_render(
danger_level=5, danger_name="Extreme",
zone_name="Western Mountains",
center_id="Sawtooth Avalanche Center",
travel=("Avoid all avalanche terrain today! Very dangerous conditions "
"exist across all elevations and aspects with widespread natural "
"avalanche activity likely through the afternoon and overnight."),
)
assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
# zone, level, source, and the first-sentence advice all present
assert "Western Mountains" in wire
assert "Extreme (5)" in wire
assert "Sawtooth Avalanche Center" in wire
assert "Avoid all avalanche terrain today!" in wire
# first sentence terminates at the '!' -> the rest is gone
assert "Very dangerous conditions" not in wire

View file

@ -36,7 +36,7 @@ def test_list_returns_all_59_keys(client):
# 14 adapters with at least one key (itd_511 has zero -- not in the
# grouped dict because the SQL only returns rows that exist).
total = sum(len(v) for v in body.values())
assert total == 92
assert total == 96
def test_list_grouped_by_adapter(client):

View file

@ -75,7 +75,7 @@ def test_adapter_config_type_check_constrains_vocabulary(fresh_db):
def test_registry_at_59_entries():
"""v0.6-3a.1 trim: 43 CONFIG-only keys (was 77 in v0.6-3a draft)."""
assert len(REGISTRY) == 92, (
assert len(REGISTRY) == 96, (
f"REGISTRY drift guard; got {len(REGISTRY)}. "
f"If a sentence template / emoji / heuristic snuck in, it belongs in CODE not config."
)

View file

@ -0,0 +1,73 @@
"""Tests for the fire-digest broadcast kill-switch (fires.digest_broadcast_enabled).
The scheduler wiring stays intact; only the mesh EMISSION is gated. Disabled by
default -> fire_slot() dispatches nothing. Flipping the flag True re-enables it
cleanly. Per-fire wfigs alerts are a separate path and are unaffected (covered in
test_wfigs_handler.py).
"""
from __future__ import annotations
import asyncio
import time
from meshai.persistence import get_db
from meshai.notifications.scheduled.fire_digest import FireDigestScheduler
class _RecordingDispatcher:
def __init__(self):
self.calls = []
async def dispatch_scheduled_broadcast(self, *, text, source_event_table,
source_event_pk):
self.calls.append(
{"text": text, "table": source_event_table, "pk": source_event_pk})
return True
def _seed_active_fire(conn):
now = int(time.time())
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, tombstoned_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
("GATE-01", "Gatekeeper Fire", "WF", 900, None, 43.6, -116.2,
"Ada", "ID", now, now - 600, None),
)
def test_fire_slot_disabled_by_default_dispatches_nothing():
"""With defaults (digest_broadcast_enabled=False) fire_slot emits nothing."""
conn = get_db()
_seed_active_fire(conn) # a broadcast WOULD be produced if the gate were open
now = int(time.time())
dispatcher = _RecordingDispatcher()
sched = FireDigestScheduler(dispatcher, clock=lambda: now)
result = asyncio.run(sched.fire_slot(now, "06:00"))
assert result is False
assert dispatcher.calls == [], "digest broadcast must NOT be dispatched by default"
def test_fire_slot_dispatches_when_flag_enabled():
"""Flipping fires.digest_broadcast_enabled True cleanly re-enables emission."""
from meshai.adapter_config import set_runtime_override
conn = get_db()
_seed_active_fire(conn)
now = int(time.time())
dispatcher = _RecordingDispatcher()
sched = FireDigestScheduler(dispatcher, clock=lambda: now)
set_runtime_override("fires", "digest_broadcast_enabled", True)
try:
result = asyncio.run(sched.fire_slot(now, "06:00"))
finally:
# Reset so the override doesn't leak into other tests in this process.
set_runtime_override("fires", "digest_broadcast_enabled", False)
assert result is True
assert len(dispatcher.calls) == 1
assert dispatcher.calls[0]["table"] == "fire_digest_broadcasts"
assert "Gatekeeper Fire" in dispatcher.calls[0]["text"]

View file

@ -33,6 +33,7 @@ import pytest
from meshai.central.incident_handler import (
handle_incident,
_render as _incident_render,
)
from meshai.persistence import close_thread_connection, init_db
from meshai.persistence import db as persistence_db
@ -216,7 +217,10 @@ def test_a_tomtom_icon_renders(mem_db, no_photon, icon, expected_emoji, expected
assert wire.startswith(f"{expected_emoji} {expected_phrase}")
assert "Near Boise, ID" in wire
assert "I-84" in wire
assert "5 min delay" in wire
# Budget-fit rework: the separate "N min delay" line was dropped; the road
# segment carries road (+ direction/lanes), and the message fits 140.
assert "min delay" not in wire
assert len(wire) <= 140
# ============================================================================
@ -774,3 +778,51 @@ def test_w_itd_511_future_scheduled_dropped_via_start_epoch(mem_db, no_photon):
now = 2_000_000_000
wire = handle_incident(env, env["subject"], data={}, now=now)
assert wire is None
# ============================================================================
# Budget-fit worst case: longest plausible traffic payload must fit 140 chars
# with critical fields (type, location, road, FULL direction word, lane
# status, trimmed narrative with intact direction word + "milepost") present.
# ============================================================================
def test_incident_worst_case_fits_140():
n = {
"sub_type": "accident",
"geocoder_city": None,
"county": "Minidoka",
"state": "ID",
"road": "SH-27",
"direction": "north", # abbreviation/short form -> MUST expand
"mile_marker": None,
"lanes_affected": "1 Right lane blocked",
"comment": (
"Southbound right lane at milepost 24 and westbound onramp to I-84 "
"blocked due to a multi-vehicle collision, expect major delays "
"through the evening commute and seek alternate routes tonight"
),
}
wire = _incident_render(n)
# (a) fits one mesh packet
assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
# (b) critical fields present
assert wire.startswith("🚨 Crash") # type
assert "Near Minidoka Co, ID" in wire # location
assert "SH-27" in wire # road
assert "Northbound" in wire # FULL direction (expanded)
assert "1 Right lane blocked" in wire # lane status
assert "SH-27 Northbound · 1 Right lane blocked" in wire # road·lane line
# (c) direction is NEVER abbreviated anywhere in the wire
for abbr in (" NB", " N ", "Sbound", "N/B"):
assert abbr not in wire
# (d) narrative present and word-boundary trimmed (no mid-word chop): the
# intact direction word "Southbound" and the intact word "milepost" survive.
assert "Southbound" in wire
assert "milepost" in wire
# trimmed from the END -> ends with the ellipsis, not raw text
assert wire.endswith("")

View file

@ -282,22 +282,25 @@ def test_svr_long_locations_path_sampled(mem_db):
d=d,
)
# (a) fits in one mesh packet
assert len(rendered) <= 200, (
f"rendered is {len(rendered)} chars (expected <= 200):\n{rendered!r}"
# (a) fits in one mesh packet (budget is now the 140-char LoRa max)
assert len(rendered) <= 140, (
f"rendered is {len(rendered)} chars (expected <= 140):\n{rendered!r}"
)
# (b) all data-point categories present
# (b) all data-point categories present, hazard wording TIGHTENED
assert "Severe Thunderstorm Warning" in rendered, "event type missing"
assert "Until" in rendered, "expiry time segment missing"
assert "Twin Falls County" in rendered, "area missing"
assert "mph winds" in rendered or "hail" in rendered, "hazard segment missing"
assert "60mph winds" in rendered, "wind hazard not tightened to '60mph winds'"
assert '1" hail' in rendered, "hail hazard not rendered as numeric inches"
assert "radar" in rendered, "certainty not collapsed to 'radar'"
assert "Moving" in rendered, "motion segment missing"
# (c) path-sampling, not tail-drop: first town, last town, and the arrow
# (c) path-sampling applied (arrow) with the soonest-impact town retained.
# At the 140 budget the farthest-along town may be trimmed by the final
# backstop; the hard cap wins over endpoint preservation.
assert "" in rendered, "no arrow -> not path-sampled"
assert "Buhl" in rendered, "first town missing"
assert "Shoshone" in rendered, "last town dropped (the old tail-trim bug)"
assert "Buhl" in rendered, "first (soonest-impact) town missing"
def test_svr_short_locations_shown_in_full(mem_db):
@ -331,6 +334,47 @@ def test_svr_short_locations_shown_in_full(mem_db):
d=d,
)
assert len(rendered) <= 200
assert len(rendered) <= 140
assert "" not in rendered, "short list should not be path-sampled"
assert "Buhl, Eden, Hazelton" in rendered, "full comma-joined list expected"
# Hazard wording is tightened even on the short-list path.
assert "60mph winds" in rendered
assert '1" hail' in rendered
assert "radar" in rendered
def test_svr_worst_case_fits_140(mem_db):
"""Pathologically long SVR payload: the final wire MUST fit 140 chars while
still carrying event name, area, time, tightened hazard, and >=1 town."""
long_locations = (
"Buhl, Eden, Hazelton, Murtaugh, Richfield, Dietrich, Gooding, "
"Hagerman, Wendell, Jerome, Kimberly, Hansen, Filer, and Shoshone"
)
description = (
"HAZARD...Damaging winds to 70 mph and golf ball size hail.\n\n"
f"Locations impacted include...{long_locations}"
)
d = {
"eventCode": {"SAME": ["SVR"]},
"certainty": "Observed",
"parameters": {
"maxWindGust": ["70 MPH"],
"maxHailSize": ["1.75"],
"eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
},
"description": description,
}
rendered = _render(
event_type="Severe Thunderstorm Warning",
area_desc="Twin Falls County",
geocoder_city=None, county="Twin Falls", state="ID",
expires_epoch=1_751_400_000, lat=42.5, lon=-114.46,
now=1_751_400_000, d=d,
)
assert len(rendered) <= 140, f"{len(rendered)} chars:\n{rendered!r}"
assert "Severe Thunderstorm Warning" in rendered # event name
assert "Twin Falls County" in rendered # area
assert "Until" in rendered # time
assert "70mph winds" in rendered # tightened hazard (wind)
assert '1.75" hail' in rendered # golf ball -> 1.75"
assert "Buhl" in rendered # >=1 town present

View file

@ -173,3 +173,21 @@ def test_commit_callback_updates_last_broadcast(mem_db):
"SELECT last_broadcast_at FROM quake_events WHERE event_id='cb1'"
).fetchone()
assert post["last_broadcast_at"] == 1_000_001
# ============================================================================
# Budget-fit SAFETY CAP: a freak-long USGS place string must still fit 140.
# ============================================================================
from meshai.central.quake_handler import _render as _quake_render
def test_quake_render_worst_case_fits_140():
place = ("293 km SSW of a pathologically long place description island "
"region in the remote northern pacific ocean near absolutely nowhere "
"at all off the coast of the far edge of the map")
wire = _quake_render(mag=7.9, place=place, depth_km=12, lat=44.123,
lon=-114.987, tsunami=True, is_update=False)
assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
# magnitude survives on the (critical) first line
assert "M7.9" in wire

View file

@ -171,3 +171,24 @@ class TestSatpassHandler:
result = handle_satpass(env, "central.sat.pass.iss", data={}, now=1718163120)
assert result is None
# ============================================================================
# Budget-fit SAFETY CAP: a pathologically long satellite name must not push
# the broadcast string past 140 chars. Calls format_pass directly (bypasses
# the broken consolidation path).
# ============================================================================
def test_format_pass_worst_case_fits_140():
from meshai.central.satpass_handler import format_pass
wire = format_pass(
sat_name=("NOAA-19 EXPERIMENTAL SUPER LONG SATELLITE DESIGNATION "
"PAYLOAD REVISION X PROTOTYPE FLIGHT MODEL SERIAL 00042"),
max_el=78.0,
aos_epoch=1719900000, los_epoch=1719900780,
aos_compass="NNW", los_compass="SSE",
entry_observer="Treasure Valley Observatory West Ridge Site",
exit_observer="Magic Valley Observatory East Rim Station",
broadcast=True,
)
assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"

View file

@ -23,6 +23,7 @@ from meshai import central_normalizer as cn
from meshai.central.wfigs_handler import (
WFIGS_BROADCAST_COOLDOWN_S,
handle_wfigs,
_render as _wfigs_render,
)
from meshai.persistence import close_thread_connection, init_db
from meshai.persistence import db as persistence_db
@ -283,7 +284,10 @@ def test_g_new_irwin_inserts_and_broadcasts(mem_db, no_photon):
assert wire.startswith("🔥 Cache Peak Fire — New")
assert "Burley" in wire
assert "1,847 ac" in wire
assert "23% contained" in wire
assert "containment 23%" in wire
# Budget-fit rework: no unique-fire-id line, no bold markdown.
assert "ID:" not in wire
assert "**" not in wire
# v0.5.8b: handler INSERTs the fires row with last_broadcast_*=NULL,
# then attaches a commit callback. The dispatcher fires the callback
@ -416,7 +420,7 @@ def test_j_known_irwin_change_after_cooldown_broadcasts(mem_db, no_photon):
assert out is not None
assert out.startswith("🔥 Cache Peak Fire — Update")
assert "3,000 ac" in out
assert "35% contained" in out
assert "containment 35%" in out
# Simulate dispatcher commit.
data2["_on_broadcast_committed"](float(later))
@ -615,3 +619,70 @@ def test_h_handler_attaches_audit_descriptor_and_callback(mem_db, no_photon):
assert callable(data["_on_broadcast_committed"])
assert data["_broadcast_audit"]["table"] == "fires"
assert data["_broadcast_audit"]["pk"] == _IRWIN_B
# ============================================================================
# Budget-fit worst case: longest plausible fire payload fits 140 chars, with
# no `ID:` line, no `**` markdown, and discovery rendered DATE-ONLY.
# ============================================================================
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
# ============================================================================
# Fire-digest kill-switch does NOT touch the per-fire wfigs path: a new-fire
# envelope still produces a broadcast even though digest broadcast is disabled.
# ============================================================================
def test_per_fire_wfigs_broadcasts_while_digest_disabled(mem_db, no_photon):
from meshai.adapter_config import adapter_config
# Default posture: the twice-daily digest broadcast is OFF.
assert bool(adapter_config.fires.digest_broadcast_enabled) is False
# ... yet a new per-fire wfigs alert still broadcasts.
env = _make_active_envelope(geocoder_city="Burley")
data = {}
wire = handle_wfigs(cn.normalize(env), env, env["subject"],
data=data, now=6_000_000)
assert wire is not None
assert wire.startswith("🔥 Cache Peak Fire — New")