mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-10 17:04:45 +02:00
feat(v0.5.10): nws + usgs_quake + swpc handlers
Three more per-adapter handlers landing in the same v0.5.9-incident-pipeline pattern: nws_handler.py with severity-floor gate (Warning+ broadcasts only, Moderate/Minor/Unknown skipped to event_log handled=0), event-type emoji map, CAP-id-based first-sight dedup via nws_alerts table; quake_handler.py with magnitude-floor gate (M3.0 globally + M2.5 within 250mi of Idaho centroid + tsunami at any M) using Haversine for the distance check, USGS data.place curated string preferred for the place anchor, leading emoji escalation (🌐 routine / ⚠️ M5+ / 🚨 tsunami), Magnitude spelled out per Matts call; swpc_handler.py with aggressive G3+/R3+/S1+ gate, plain-English wire headlines with (NOAA scale / underlying scalar) tail tag per Matts option C (e.g. "Strong geomagnetic storm (G3/Kp7) -- HF degraded, aurora possible"), routine Kp + protons persisted to swpc_events.payload_json for trending but never broadcast. All three share the v0.5.9 universal freshness gate and the no-Update first-sight-only pattern. Persistence uses the existing v0.5.8b nws_alerts, quake_events, swpc_events tables -- no migrations needed. Tests: was 634 (v0.5.9 baseline), now 686 (+52 net new; over-delivered because parametrized emoji map adds 14 rows). Synthetic probe over the 4 nws + 1 quake + 16,217 swpc captured envelopes from the batched investigation: Phase 1 = 0/0/0 broadcasts (all real captures correctly filtered by their respective gates); Phase 2 = 5/5 synthesized fresh test events broadcast correctly (Severe T-Storm warning, M4.1 Garden Valley quake, G3 geomagnetic storm, X1.2 flare, S1 proton). WFIGS handler unchanged. usgs_nwis deferred to v0.5.12 (threshold-curation work). Master OFF in prod. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0099d0fd94
commit
de35f9c748
8 changed files with 1496 additions and 0 deletions
201
tests/test_nws_handler.py
Normal file
201
tests/test_nws_handler.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
"""Tests for v0.5.10 NWS handler."""
|
||||
import pytest
|
||||
|
||||
from meshai.central.nws_handler import handle_nws, _emoji_for_event
|
||||
from meshai.persistence import close_thread_connection, init_db
|
||||
from meshai.persistence import db as persistence_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mem_db(monkeypatch, tmp_path):
|
||||
db_path = str(tmp_path / "nws-test.sqlite")
|
||||
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
|
||||
persistence_db._initialised.clear()
|
||||
close_thread_connection()
|
||||
conn = init_db()
|
||||
yield conn
|
||||
close_thread_connection()
|
||||
persistence_db._initialised.discard(db_path)
|
||||
|
||||
|
||||
def _nws_env(*, cap_id="urn:oid:test.001",
|
||||
event="Severe Thunderstorm Warning",
|
||||
severity_str="Severe",
|
||||
area_desc="Twin Falls County",
|
||||
county="Twin Falls", state="ID",
|
||||
expires="2026-06-05T03:00:00Z",
|
||||
msg_type=None,
|
||||
lat=42.500, lon=-114.460,
|
||||
geocoder_city=None,
|
||||
category="wx.alert.severe_thunderstorm_warning"):
|
||||
return {
|
||||
"id": cap_id, "subject": "central.wx.alert.us.id",
|
||||
"data": {
|
||||
"id": cap_id, "adapter": "nws", "category": category,
|
||||
"severity": 2,
|
||||
"geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
|
||||
"data": {
|
||||
"id": cap_id, "@type": "wx:Alert",
|
||||
"event": event, "severity": severity_str,
|
||||
"areaDesc": area_desc, "msgType": msg_type or "Alert",
|
||||
"headline": f"{event} for {area_desc}",
|
||||
"description": "Storm details.",
|
||||
"expires": expires,
|
||||
"_enriched": {"geocoder": {"city": geocoder_city,
|
||||
"county": county, "state": state}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _commit(data, t):
|
||||
data["_on_broadcast_committed"](float(t))
|
||||
|
||||
|
||||
# ---- severity gate ----
|
||||
|
||||
|
||||
def test_severe_thunderstorm_warning_broadcasts(mem_db):
|
||||
env = _nws_env(severity_str="Severe", event="Severe Thunderstorm Warning")
|
||||
data = {}
|
||||
wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert wire.startswith("🌩️")
|
||||
assert "Severe Thunderstorm Warning" in wire
|
||||
|
||||
|
||||
def test_extreme_emergency_broadcasts(mem_db):
|
||||
env = _nws_env(severity_str="Extreme", event="Tornado Warning",
|
||||
category="wx.alert.tornado_warning")
|
||||
data = {}
|
||||
wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert wire.startswith("🌪️")
|
||||
|
||||
|
||||
def test_special_weather_statement_skipped(mem_db):
|
||||
env = _nws_env(severity_str="Minor", event="Special Weather Statement",
|
||||
category="wx.alert.special_weather_statement")
|
||||
data = {}
|
||||
wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
|
||||
assert wire is None
|
||||
n_rows = mem_db.execute("SELECT COUNT(*) AS n FROM nws_alerts").fetchone()["n"]
|
||||
assert n_rows == 0
|
||||
n_log = mem_db.execute(
|
||||
"SELECT COUNT(*) AS n FROM event_log WHERE source='nws' AND handled=0"
|
||||
).fetchone()["n"]
|
||||
assert n_log == 1
|
||||
|
||||
|
||||
def test_watch_severity_moderate_skipped(mem_db):
|
||||
env = _nws_env(severity_str="Moderate", event="Severe Thunderstorm Watch",
|
||||
category="wx.alert.severe_thunderstorm_watch")
|
||||
data = {}
|
||||
wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
|
||||
assert wire is None
|
||||
|
||||
|
||||
# ---- emoji map ----
|
||||
|
||||
|
||||
@pytest.mark.parametrize("event_type, expected_emoji", [
|
||||
("Severe Thunderstorm Warning", "🌩️"),
|
||||
("Tornado Warning", "🌪️"),
|
||||
("Flash Flood Warning", "🌊"),
|
||||
("Flood Warning", "🌊"),
|
||||
("Winter Storm Warning", "❄️"),
|
||||
("Blizzard Warning", "❄️"),
|
||||
("Excessive Heat Warning", "🌡️"),
|
||||
("High Wind Warning", "🌬️"),
|
||||
("Red Flag Warning", "🔥"),
|
||||
("Fire Weather Watch", "🔥"),
|
||||
("Air Quality Alert", "😷"),
|
||||
("Freeze Warning", "🥶"),
|
||||
("Coastal Flood Warning", "🌊"),
|
||||
("(some other warning)", "⚠️"),
|
||||
])
|
||||
def test_emoji_map(event_type, expected_emoji):
|
||||
assert _emoji_for_event(event_type) == expected_emoji
|
||||
|
||||
|
||||
# ---- tombstone ----
|
||||
|
||||
|
||||
def test_cancel_msgType_tombstone_skipped(mem_db):
|
||||
env = _nws_env(severity_str="Severe", event="Severe Thunderstorm Warning",
|
||||
msg_type="Cancel")
|
||||
data = {}
|
||||
wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
|
||||
assert wire is None
|
||||
n_log = mem_db.execute(
|
||||
"SELECT COUNT(*) AS n FROM event_log WHERE source='nws' AND handled=0"
|
||||
).fetchone()["n"]
|
||||
assert n_log == 1
|
||||
|
||||
|
||||
def test_expire_msgType_tombstone_skipped(mem_db):
|
||||
env = _nws_env(severity_str="Severe", event="Tornado Warning",
|
||||
msg_type="Expire")
|
||||
wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
|
||||
|
||||
# ---- per-CAP-id dedup ----
|
||||
|
||||
|
||||
def test_per_cap_id_dedup_no_reissue(mem_db):
|
||||
env = _nws_env(severity_str="Severe")
|
||||
data1 = {}
|
||||
wire1 = handle_nws(env, env["subject"], data=data1, now=1_000_000)
|
||||
assert wire1 is not None
|
||||
_commit(data1, 1_000_001)
|
||||
|
||||
# Same CAP id republishes (e.g. headline update). Should NOT re-broadcast.
|
||||
data2 = {}
|
||||
wire2 = handle_nws(env, env["subject"], data=data2, now=1_000_300)
|
||||
assert wire2 is None
|
||||
|
||||
|
||||
# ---- area_desc fallback ----
|
||||
|
||||
|
||||
def test_area_desc_used_when_geocoder_city_missing(mem_db):
|
||||
env = _nws_env(severity_str="Severe", area_desc="Twin Falls County",
|
||||
geocoder_city=None)
|
||||
wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
|
||||
assert "Twin Falls" in wire
|
||||
|
||||
|
||||
def test_geocoder_city_preferred_over_area_desc(mem_db):
|
||||
env = _nws_env(severity_str="Severe", area_desc="Twin Falls County",
|
||||
geocoder_city="Twin Falls")
|
||||
wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
|
||||
assert "Twin Falls" in wire # either source serves the same anchor
|
||||
|
||||
|
||||
# ---- commit callback ----
|
||||
|
||||
|
||||
def test_commit_callback_updates_last_broadcast(mem_db):
|
||||
env = _nws_env(severity_str="Severe")
|
||||
data = {}
|
||||
handle_nws(env, env["subject"], data=data, now=1_000_000)
|
||||
fr_pre = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM nws_alerts").fetchone()
|
||||
assert fr_pre["last_broadcast_at"] is None
|
||||
_commit(data, 1_000_001)
|
||||
fr_post = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM nws_alerts").fetchone()
|
||||
assert fr_post["last_broadcast_at"] == 1_000_001
|
||||
# event_log row flipped to handled=1.
|
||||
el = mem_db.execute(
|
||||
"SELECT handled FROM event_log WHERE source='nws' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
assert el["handled"] == 1
|
||||
|
||||
|
||||
def test_wire_includes_coords_and_expires(mem_db):
|
||||
env = _nws_env(severity_str="Severe", lat=42.500, lon=-114.460)
|
||||
wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
|
||||
assert "@ 42.500,-114.460" in wire
|
||||
assert "until" in wire.lower()
|
||||
175
tests/test_quake_handler.py
Normal file
175
tests/test_quake_handler.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
"""Tests for v0.5.10 USGS earthquakes handler."""
|
||||
import pytest
|
||||
|
||||
from meshai.central.quake_handler import (
|
||||
handle_quake,
|
||||
within_250mi_of_idaho,
|
||||
)
|
||||
from meshai.persistence import close_thread_connection, init_db
|
||||
from meshai.persistence import db as persistence_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mem_db(monkeypatch, tmp_path):
|
||||
db_path = str(tmp_path / "quake-test.sqlite")
|
||||
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
|
||||
persistence_db._initialised.clear()
|
||||
close_thread_connection()
|
||||
conn = init_db()
|
||||
yield conn
|
||||
close_thread_connection()
|
||||
persistence_db._initialised.discard(db_path)
|
||||
|
||||
|
||||
def _quake_env(*, event_id="uu80141266", mag=3.5, depth_km=9.0,
|
||||
place="9 km SW of Stanley, Idaho",
|
||||
lat=44.094, lon=-115.962,
|
||||
tsunami=0, alert=None,
|
||||
time_ms=1780006952030,
|
||||
category="quake.event.minor"):
|
||||
return {
|
||||
"id": event_id, "subject": "central.quake.event.minor.unknown",
|
||||
"data": {
|
||||
"id": event_id, "adapter": "usgs_quake", "category": category,
|
||||
"severity": 0,
|
||||
"geo": {"centroid": [lon, lat], "primary_region": None},
|
||||
"data": {
|
||||
"id": event_id, "magnitude": mag, "place": place,
|
||||
"depth_km": depth_km, "time_ms": time_ms,
|
||||
"tsunami": tsunami, "alert": alert, "status": "reviewed",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _commit(data, t):
|
||||
data["_on_broadcast_committed"](float(t))
|
||||
|
||||
|
||||
# ---- magnitude floor ----
|
||||
|
||||
|
||||
def test_m3_anywhere_broadcasts(mem_db):
|
||||
env = _quake_env(mag=3.5, lat=37.0, lon=-122.0) # SF Bay area, outside Idaho
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "Magnitude 3.5" in wire
|
||||
|
||||
|
||||
def test_m25_inside_idaho_broadcasts(mem_db):
|
||||
env = _quake_env(mag=2.7, lat=44.094, lon=-115.962, event_id="uu1")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "Magnitude 2.7" in wire
|
||||
|
||||
|
||||
def test_m25_outside_idaho_skipped(mem_db):
|
||||
# San Francisco -- well outside 250mi of Idaho centroid.
|
||||
env = _quake_env(mag=2.7, lat=37.0, lon=-122.0, event_id="uu2")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
|
||||
|
||||
def test_below_25_skipped(mem_db):
|
||||
env = _quake_env(mag=1.01, lat=44.0, lon=-114.0, event_id="uu3")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
|
||||
|
||||
# ---- tsunami special ----
|
||||
|
||||
|
||||
def test_tsunami_any_magnitude_broadcasts(mem_db):
|
||||
env = _quake_env(mag=4.5, lat=10.0, lon=140.0, tsunami=1, event_id="japan1")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "TSUNAMI WARNING" in wire
|
||||
assert wire.startswith("🚨")
|
||||
|
||||
|
||||
# ---- PAGER alert ----
|
||||
|
||||
|
||||
def test_pager_orange_broadcasts(mem_db):
|
||||
env = _quake_env(mag=2.0, lat=37.0, lon=-122.0, alert="orange",
|
||||
event_id="pager1")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
|
||||
|
||||
def test_pager_red_broadcasts(mem_db):
|
||||
env = _quake_env(mag=2.0, lat=37.0, lon=-122.0, alert="red",
|
||||
event_id="pager2")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
|
||||
|
||||
# ---- wire format ----
|
||||
|
||||
|
||||
def test_uses_usgs_place_string(mem_db):
|
||||
env = _quake_env(mag=4.1, place="9 km SW of Stanley, Idaho",
|
||||
event_id="usgs1")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert "9 km SW of Stanley, Idaho" in wire
|
||||
|
||||
|
||||
def test_m5_uses_warning_emoji(mem_db):
|
||||
env = _quake_env(mag=5.2, lat=44.0, lon=-114.0, event_id="big1")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire.startswith("⚠️")
|
||||
|
||||
|
||||
def test_wire_includes_depth_and_coords(mem_db):
|
||||
env = _quake_env(mag=4.1, depth_km=9.0, lat=44.094, lon=-115.962,
|
||||
event_id="d1")
|
||||
wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
|
||||
assert "9km depth" in wire
|
||||
assert "@ 44.094,-115.962" in wire
|
||||
|
||||
|
||||
# ---- per-event dedup ----
|
||||
|
||||
|
||||
def test_per_event_id_dedup_no_reissue(mem_db):
|
||||
env = _quake_env(mag=4.0, event_id="dedup1")
|
||||
data1 = {}
|
||||
handle_quake(env, env["subject"], data=data1, now=1_000_000)
|
||||
_commit(data1, 1_000_001)
|
||||
|
||||
# Same event_id republishes (magnitude revision). Should NOT re-broadcast.
|
||||
env_rev = _quake_env(mag=4.2, event_id="dedup1") # higher mag, same id
|
||||
wire2 = handle_quake(env_rev, env_rev["subject"], data={}, now=1_000_300)
|
||||
assert wire2 is None
|
||||
|
||||
|
||||
# ---- distance helper ----
|
||||
|
||||
|
||||
def test_within_250mi_of_idaho_boundary():
|
||||
# Boise, ID -- inside
|
||||
assert within_250mi_of_idaho(43.6, -116.2) is True
|
||||
# San Francisco -- outside
|
||||
assert within_250mi_of_idaho(37.0, -122.0) is False
|
||||
# Seattle -- inside (250mi from Idaho centroid; verify the boundary)
|
||||
assert within_250mi_of_idaho(47.6, -122.3) is False
|
||||
# Boundary edge case (Idaho center)
|
||||
assert within_250mi_of_idaho(44.36, -114.61) is True
|
||||
|
||||
|
||||
# ---- commit callback ----
|
||||
|
||||
|
||||
def test_commit_callback_updates_last_broadcast(mem_db):
|
||||
env = _quake_env(mag=4.0, event_id="cb1")
|
||||
data = {}
|
||||
handle_quake(env, env["subject"], data=data, now=1_000_000)
|
||||
pre = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM quake_events WHERE event_id='cb1'"
|
||||
).fetchone()
|
||||
assert pre["last_broadcast_at"] is None
|
||||
_commit(data, 1_000_001)
|
||||
post = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM quake_events WHERE event_id='cb1'"
|
||||
).fetchone()
|
||||
assert post["last_broadcast_at"] == 1_000_001
|
||||
218
tests/test_swpc_handler.py
Normal file
218
tests/test_swpc_handler.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
"""Tests for v0.5.10 SWPC space-weather handler."""
|
||||
import pytest
|
||||
|
||||
from meshai.central.swpc_handler import handle_swpc
|
||||
from meshai.persistence import close_thread_connection, init_db
|
||||
from meshai.persistence import db as persistence_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mem_db(monkeypatch, tmp_path):
|
||||
db_path = str(tmp_path / "swpc-test.sqlite")
|
||||
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
|
||||
persistence_db._initialised.clear()
|
||||
close_thread_connection()
|
||||
conn = init_db()
|
||||
yield conn
|
||||
close_thread_connection()
|
||||
persistence_db._initialised.discard(db_path)
|
||||
|
||||
|
||||
def _kindex_env(*, kp=3.0, event_id="kp_2026_06_05_15Z"):
|
||||
return {
|
||||
"id": event_id, "subject": "central.space.kindex",
|
||||
"data": {
|
||||
"id": event_id, "adapter": "swpc_kindex",
|
||||
"category": "space.kindex", "severity": 0,
|
||||
"geo": {},
|
||||
"data": {"id": event_id, "kp_index": kp,
|
||||
"time": "2026-06-05T15:00:00Z"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _protons_env(*, flux=1.0, event_id="p_2026_06_05_15Z"):
|
||||
return {
|
||||
"id": event_id, "subject": "central.space.proton_flux",
|
||||
"data": {
|
||||
"id": event_id, "adapter": "swpc_protons",
|
||||
"category": "space.proton_flux", "severity": 0,
|
||||
"geo": {},
|
||||
"data": {"id": event_id, "p10mev": flux,
|
||||
"time": "2026-06-05T15:00:00Z"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _alert_env(*, flare_class=None, kp=None, pfu=None,
|
||||
event_id="alert_001", product_id="ALTPRO"):
|
||||
d = {"id": event_id, "product_id": product_id,
|
||||
"time": "2026-06-05T15:00:00Z"}
|
||||
if flare_class: d["flare_class"] = flare_class
|
||||
if kp: d["kp_index"] = kp
|
||||
if pfu: d["p10mev"] = pfu
|
||||
return {
|
||||
"id": event_id, "subject": "central.space.alert.xrayflare",
|
||||
"data": {
|
||||
"id": event_id, "adapter": "swpc_alerts",
|
||||
"category": "space.alert", "severity": 1,
|
||||
"geo": {}, "data": d,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _commit(data, t):
|
||||
data["_on_broadcast_committed"](float(t))
|
||||
|
||||
|
||||
# ---- geomagnetic storm ----
|
||||
|
||||
|
||||
def test_kp_below_7_skipped(mem_db):
|
||||
env = _kindex_env(kp=4.0, event_id="kp_low")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
# Row persisted for trending, not broadcast.
|
||||
row = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM swpc_events WHERE event_id='kp_low'"
|
||||
).fetchone()
|
||||
assert row is not None
|
||||
assert row["last_broadcast_at"] is None
|
||||
|
||||
|
||||
def test_kp7_g3_broadcasts(mem_db):
|
||||
env = _kindex_env(kp=7.0, event_id="kp_g3")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert wire.startswith("🌌")
|
||||
assert "G3" in wire
|
||||
assert "Kp7" in wire
|
||||
assert "geomagnetic storm" in wire.lower()
|
||||
|
||||
|
||||
def test_kp9_g5_broadcasts_with_extreme_label(mem_db):
|
||||
env = _kindex_env(kp=9.0, event_id="kp_g5")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "G5" in wire
|
||||
assert "extreme" in wire.lower()
|
||||
|
||||
|
||||
# ---- solar flares ----
|
||||
|
||||
|
||||
def test_m_class_flare_skipped(mem_db):
|
||||
env = _alert_env(flare_class="M5.5", event_id="m55_flare")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
|
||||
|
||||
def test_x1_flare_r3_broadcasts(mem_db):
|
||||
env = _alert_env(flare_class="X1.2", event_id="x1_flare")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert wire.startswith("🔆")
|
||||
assert "R3" in wire
|
||||
assert "X1.2" in wire
|
||||
|
||||
|
||||
def test_x10_flare_r4_broadcasts(mem_db):
|
||||
env = _alert_env(flare_class="X10", event_id="x10_flare")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "R4" in wire or "R5" in wire
|
||||
|
||||
|
||||
def test_flare_class_in_product_id(mem_db):
|
||||
"""Some swpc_alerts encode the class in product_id rather than flare_class."""
|
||||
env = _alert_env(event_id="prod_id_flare", product_id="X2.1 FLARE EVENT")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "R3" in wire
|
||||
|
||||
|
||||
# ---- proton events ----
|
||||
|
||||
|
||||
def test_proton_below_threshold_skipped(mem_db):
|
||||
env = _protons_env(flux=0.5, event_id="p_low")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
row = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM swpc_events WHERE event_id='p_low'"
|
||||
).fetchone()
|
||||
assert row is not None
|
||||
assert row["last_broadcast_at"] is None
|
||||
|
||||
|
||||
def test_proton_s1_threshold_broadcasts(mem_db):
|
||||
env = _protons_env(flux=15, event_id="p_s1")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert wire.startswith("☢️")
|
||||
assert "S1" in wire
|
||||
|
||||
|
||||
def test_proton_s2_broadcasts(mem_db):
|
||||
env = _protons_env(flux=200, event_id="p_s2")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is not None
|
||||
assert "S2" in wire
|
||||
|
||||
|
||||
# ---- wire format ----
|
||||
|
||||
|
||||
def test_wire_has_scale_code_and_scalar_tail(mem_db):
|
||||
env = _kindex_env(kp=7.0, event_id="fmt1")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
# Matt's format: "🌌 Strong geomagnetic storm (G3/Kp7) -- HF degraded, ..."
|
||||
assert "(G3/Kp7)" in wire
|
||||
assert "--" in wire
|
||||
|
||||
|
||||
# ---- per-event dedup ----
|
||||
|
||||
|
||||
def test_per_event_dedup_no_reissue(mem_db):
|
||||
env = _kindex_env(kp=7.0, event_id="dedup_kp")
|
||||
data1 = {}
|
||||
handle_swpc(env, env["subject"], data=data1, now=1_000_000)
|
||||
_commit(data1, 1_000_001)
|
||||
# Re-publish with same id and same Kp -- should not re-broadcast.
|
||||
wire2 = handle_swpc(env, env["subject"], data={}, now=1_000_300)
|
||||
assert wire2 is None
|
||||
|
||||
|
||||
# ---- commit callback ----
|
||||
|
||||
|
||||
def test_commit_callback_updates_last_broadcast(mem_db):
|
||||
env = _kindex_env(kp=7.0, event_id="cb_swpc")
|
||||
data = {}
|
||||
handle_swpc(env, env["subject"], data=data, now=1_000_000)
|
||||
pre = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM swpc_events WHERE event_id='cb_swpc'"
|
||||
).fetchone()
|
||||
assert pre["last_broadcast_at"] is None
|
||||
_commit(data, 1_000_001)
|
||||
post = mem_db.execute(
|
||||
"SELECT last_broadcast_at FROM swpc_events WHERE event_id='cb_swpc'"
|
||||
).fetchone()
|
||||
assert post["last_broadcast_at"] == 1_000_001
|
||||
|
||||
|
||||
# ---- routine readings persist but never broadcast ----
|
||||
|
||||
|
||||
def test_routine_kp_reading_persists_no_broadcast(mem_db):
|
||||
"""Sub-G3 Kp must still be saved for trending queries."""
|
||||
env = _kindex_env(kp=4.5, event_id="routine_kp")
|
||||
wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
|
||||
assert wire is None
|
||||
row = mem_db.execute(
|
||||
"SELECT event_type, payload_json FROM swpc_events "
|
||||
"WHERE event_id='routine_kp'").fetchone()
|
||||
assert row is not None
|
||||
assert row["event_type"] == "swpc_kindex"
|
||||
assert "kp_index" in row["payload_json"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue