mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
177 lines
6.4 KiB
Python
177 lines
6.4 KiB
Python
|
|
"""v0.6-4 — B13 guard-commit ordering + dedup suffix.
|
||
|
|
|
||
|
|
Regression tests for two silent-suppression defects:
|
||
|
|
|
||
|
|
1. B13 ordering: cooldown arming and dedup recording used to happen
|
||
|
|
BEFORE the region filter / severity floor / matrix resolution and
|
||
|
|
BEFORE delivery. A non-deliverable or failed event therefore burned
|
||
|
|
guard state and suppressed later deliverable events for up to the
|
||
|
|
dedup retention window. Now both commit only after >=1 successful
|
||
|
|
delivery.
|
||
|
|
|
||
|
|
2. Dedup suffix: feeds like WFIGS publish the SAME upstream id
|
||
|
|
(IrwinID) on every sweep, so the bare (source, id) dedup key
|
||
|
|
permanently swallowed every post-"New" lifecycle broadcast. The
|
||
|
|
handler now stamps the broadcast-justifying state into
|
||
|
|
event.data["_dedup_suffix"]; unchanged repeats dedup, updates pass.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from meshai.config import Config
|
||
|
|
from meshai.notifications.pipeline.dispatcher import Dispatcher
|
||
|
|
from meshai.notifications.events import make_event
|
||
|
|
|
||
|
|
|
||
|
|
class RecChannel:
|
||
|
|
def __init__(self, rec, succeed=True):
|
||
|
|
self.rec = rec
|
||
|
|
self.succeed = succeed
|
||
|
|
|
||
|
|
async def deliver(self, payload, rule):
|
||
|
|
self.rec.append({
|
||
|
|
"delivery_type": rule.delivery_type,
|
||
|
|
"message": payload.message,
|
||
|
|
})
|
||
|
|
return self.succeed
|
||
|
|
|
||
|
|
|
||
|
|
def _cfg(**kw):
|
||
|
|
cfg = Config()
|
||
|
|
cfg.notifications.rules = []
|
||
|
|
cfg.notifications.cold_start_grace_seconds = 0
|
||
|
|
t = cfg.notifications.toggles["weather"]
|
||
|
|
t.enabled = True
|
||
|
|
t.min_severity = kw.get("min_severity", "routine")
|
||
|
|
t.regions = kw.get("regions", [])
|
||
|
|
t.severity_channels = kw.get("severity_channels", {
|
||
|
|
"routine": ["mesh_broadcast"],
|
||
|
|
"priority": ["mesh_broadcast"],
|
||
|
|
"immediate": ["mesh_broadcast"],
|
||
|
|
})
|
||
|
|
t.freshness_seconds = 600
|
||
|
|
t.cooldown_seconds = kw.get("cooldown_seconds", 300)
|
||
|
|
t.broadcast_channel = 1
|
||
|
|
return cfg
|
||
|
|
|
||
|
|
|
||
|
|
def _ev(eid="ev-1", severity="priority", **data):
|
||
|
|
ev = make_event(source="nws", category="weather.alert.severe",
|
||
|
|
severity=severity, title="t")
|
||
|
|
ev.id = eid
|
||
|
|
if data:
|
||
|
|
ev.data.update(data)
|
||
|
|
return ev
|
||
|
|
|
||
|
|
|
||
|
|
def _disp(cfg, succeed=True):
|
||
|
|
rec: list = []
|
||
|
|
d = Dispatcher(cfg, lambda rule, conn: RecChannel(rec, succeed), connector=None)
|
||
|
|
return d, rec
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- B13
|
||
|
|
|
||
|
|
|
||
|
|
def test_empty_matrix_row_burns_no_guard_state():
|
||
|
|
"""Event whose severity row is empty must leave no cooldown/dedup."""
|
||
|
|
cfg = _cfg(severity_channels={"routine": [], "priority": [],
|
||
|
|
"immediate": ["mesh_broadcast"]})
|
||
|
|
d, rec = _disp(cfg)
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="fire-x", severity="priority")))
|
||
|
|
assert rec == []
|
||
|
|
assert len(d._toggle_cooldown) == 0
|
||
|
|
assert len(d._dedup_lru) == 0
|
||
|
|
# Operator fixes the matrix -> the SAME event id must now deliver.
|
||
|
|
cfg.notifications.toggles["weather"].severity_channels["priority"] = \
|
||
|
|
["mesh_broadcast"]
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="fire-x", severity="priority")))
|
||
|
|
assert len(rec) == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_below_severity_floor_burns_no_guard_state():
|
||
|
|
cfg = _cfg(min_severity="immediate")
|
||
|
|
d, rec = _disp(cfg)
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="ev-floor", severity="priority")))
|
||
|
|
assert rec == [] and not d._toggle_cooldown and not d._dedup_lru
|
||
|
|
|
||
|
|
|
||
|
|
def test_wrong_region_burns_no_guard_state():
|
||
|
|
cfg = _cfg(regions=["US-ID"])
|
||
|
|
ev = _ev(eid="ev-region", severity="priority")
|
||
|
|
ev.region = "US-MT"
|
||
|
|
d, rec = _disp(cfg)
|
||
|
|
asyncio.run(d.dispatch(ev))
|
||
|
|
assert rec == [] and not d._toggle_cooldown and not d._dedup_lru
|
||
|
|
|
||
|
|
|
||
|
|
def test_failed_delivery_leaves_no_dedup_and_retries():
|
||
|
|
"""All-channels-failed delivery must not dedup-record: the next
|
||
|
|
sweep's redelivery of the same id must reach the channel again."""
|
||
|
|
cfg = _cfg()
|
||
|
|
d, rec = _disp(cfg, succeed=False)
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="retry-me", severity="priority")))
|
||
|
|
assert len(rec) == 1 # attempted
|
||
|
|
assert not d._dedup_lru # but not recorded
|
||
|
|
assert not d._toggle_cooldown # and no cooldown armed
|
||
|
|
# Channel recovers -> same event id delivers.
|
||
|
|
d._channel_factory = lambda rule, conn: RecChannel(rec, succeed=True)
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="retry-me", severity="priority")))
|
||
|
|
assert len(rec) == 2
|
||
|
|
assert d._dedup_lru # recorded only on success
|
||
|
|
|
||
|
|
|
||
|
|
def test_successful_delivery_commits_cooldown_and_dedup():
|
||
|
|
cfg = _cfg()
|
||
|
|
d, rec = _disp(cfg)
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="ok-1", severity="priority")))
|
||
|
|
assert len(rec) == 1
|
||
|
|
assert len(d._toggle_cooldown) == 1
|
||
|
|
assert ("nws", "ok-1") in d._dedup_lru
|
||
|
|
# Exact repeat is throttled (cooldown fires first at this severity).
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="ok-1", severity="priority")))
|
||
|
|
assert len(rec) == 1
|
||
|
|
assert d._cooldown_dropped == 1
|
||
|
|
# With cooldown out of the way, the repeat is dedup-dropped.
|
||
|
|
cfg.notifications.toggles["weather"].cooldown_seconds = 0
|
||
|
|
asyncio.run(d.dispatch(_ev(eid="ok-1", severity="priority")))
|
||
|
|
assert len(rec) == 1
|
||
|
|
assert d._dedup_dropped == 1
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- suffix
|
||
|
|
|
||
|
|
|
||
|
|
def test_dedup_suffix_lets_updates_pass_and_repeats_dedup():
|
||
|
|
"""Same upstream id: identical suffix dedups, changed suffix passes —
|
||
|
|
the WFIGS lifecycle-update fix."""
|
||
|
|
cfg = _cfg(cooldown_seconds=0)
|
||
|
|
d, rec = _disp(cfg)
|
||
|
|
# "New" broadcast: 0.1 acres.
|
||
|
|
asyncio.run(d.dispatch(
|
||
|
|
_ev(eid="{IRWIN-1}", severity="immediate", _dedup_suffix="0.1|0")))
|
||
|
|
assert len(rec) == 1
|
||
|
|
# Feed re-sweeps, nothing changed: same id + same suffix -> dedup.
|
||
|
|
asyncio.run(d.dispatch(
|
||
|
|
_ev(eid="{IRWIN-1}", severity="immediate", _dedup_suffix="0.1|0")))
|
||
|
|
assert len(rec) == 1
|
||
|
|
assert d._dedup_dropped == 1
|
||
|
|
# The fire blows up: same id, NEW suffix -> must broadcast.
|
||
|
|
asyncio.run(d.dispatch(
|
||
|
|
_ev(eid="{IRWIN-1}", severity="immediate", _dedup_suffix="9400.0|10")))
|
||
|
|
assert len(rec) == 2
|
||
|
|
# And that update's repeat dedups too.
|
||
|
|
asyncio.run(d.dispatch(
|
||
|
|
_ev(eid="{IRWIN-1}", severity="immediate", _dedup_suffix="9400.0|10")))
|
||
|
|
assert len(rec) == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_wfigs_handler_stamps_dedup_suffix():
|
||
|
|
from meshai.central.wfigs_handler import _attach_commit_handles
|
||
|
|
data = {}
|
||
|
|
_attach_commit_handles(data, irwin_id="{X}", acres=42.0,
|
||
|
|
contained_pct=15, event_log_row_id=None)
|
||
|
|
assert data["_dedup_suffix"] == "42.0|15"
|
||
|
|
assert data["_cooldown_suffix"] == "{X}"
|