meshai/work/tests/test_pipeline_grouper.py

181 lines
6.4 KiB
Python
Raw Normal View History

test: fix 13 stale tests in the red suite, leave 6 real-bug failures (#124) (#129) Triaged all 20 known-red tests. 13 were stale tests asserting rotted expectations against deliberate, documented behavior changes; fixed by deriving expected values instead of hard-coding, or updating the expectation to match a documented policy change: - test_adapter_config_foundation.py / test_adapter_config_api.py: REGISTRY/API key-count and key-set guards hard-coded magic numbers (59/94/17) that rotted repeatedly. Now derive expectations from REGISTRY itself and, for the schema version, from the migrations directory, so they can't rot the same way again. - test_fire_tracker_phase4.py: two tests hardcoded a nonexistent deployment path (/opt/meshai/meshai/router.py) that matches no Dockerfile WORKDIR in this repo; resolve the module path via importlib.util.find_spec instead. - test_tombstone_broadcast.py: asserted fire severity == "immediate", which commit 2f677e85 deliberately downgraded to "priority" (to stop fire broadcasts bypassing the Grouper/cooldown during NATS backlog replay) without updating this test. - test_pipeline_grouper.py: test_immediate_severity_bypasses_grouper asserted an immediate-severity bypass that commit 85d48ce3 ("fix(fire): remove immediate-severity exemption from grouper + cooldown") DELETED on purpose -- fire events carry _severity_override="immediate", and the exemption left fire with no rate control at all in normal live operation. Re-adding the bypass would re-open that fire-spam hole on a public-safety mesh, so the test moves, not the source. Renamed + inverted to assert the real contract (all severities coalesce; only a missing group_key passes through). - test_tail_followups.py: dispatcher mock was missing dispatch_scheduled_fire_broadcast (a method added alongside the generic dispatch_scheduled_broadcast; test_reminders.py already mocks both). - test_tracking_v057.py: guard required an empty tracking-family adapter list in Environment.tsx, but the frontend has long grouped the pre-existing native satpass adapter under the "Tracking" display section (its own "satpass" backend toggle, unrelated to the Phase-7 tracking family every other guard in this file confirms is still unimplemented). Narrowed the guard to allow only that known entry. - test_v052_dispatcher.py: two tests used category="wildfire_incident", which the phase3b fire migration (#33) forced onto a dedicated formatter via NATIVE_ALWAYS_DECIDE; swapped to wildfire_hotspot (same emoji/label, not in NATIVE_ALWAYS_DECIDE) to keep exercising the generic composer logic under test. Also fixes one stale COMMENT (comment-only, no logic change) in meshai/notifications/pipeline/__init__.py's start_pipeline(): it still claimed "Immediate events bypass the grouper and don't need this [periodic flush]", which has been false since 85d48ce3 and is precisely what makes the deleted bypass look like a missing feature. The comment now records that the removal was deliberate and must not be reverted. The remaining 6 failures are left untouched -- 2 confirmed real bugs, to be fixed deliberately in their own changes: - meshcore_transport.py defines `_resolve_contact` TWICE on MeshCoreTransport (line 171 from PR #56, line 1227 from PR #92). The second silently shadows the first, so the DM contact-resolution refetch-on-miss that #56 added is dead code in production. (3 tests) - SCHEMA_VERSION (persistence/db.py:33) is stale at 26 vs. the actual highest migration v28; v27 and v28 shipped without bumping it. (3 tests) Plus 1 environment gap, not a code defect: test_natural_language_fire_question_routes_to_llm needs the `openai` package, which is declared in requirements.txt but not installed here. Suite: 20 failed, 2240 passed, 72 skipped -> 7 failed, 2254 passed, 72 skipped. All 7 remaining failures are ones classified above; no new failures introduced elsewhere in the suite. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:36:19 -06:00
"""Grouper tests: coalescing (all severities) + periodic flush.
Note: there is deliberately NO immediate-severity bypass -- commit 85d48ce3
removed it so fire broadcasts obey rate control. The only pass-through is
"event has no group_key".
"""
from meshai.notifications.pipeline.grouper import Grouper
from meshai.notifications.events import make_event
class Recorder:
def __init__(self):
self.received = []
def handle(self, event):
self.received.append(event)
def _ev(severity, group_key="gk1"):
return make_event(
source="usgs_quake",
category="earthquake_event",
severity=severity,
title=f"test {severity}",
lat=42.6,
lon=-114.5,
group_key=group_key,
inhibit_keys=[group_key],
)
test: fix 13 stale tests in the red suite, leave 6 real-bug failures (#124) (#129) Triaged all 20 known-red tests. 13 were stale tests asserting rotted expectations against deliberate, documented behavior changes; fixed by deriving expected values instead of hard-coding, or updating the expectation to match a documented policy change: - test_adapter_config_foundation.py / test_adapter_config_api.py: REGISTRY/API key-count and key-set guards hard-coded magic numbers (59/94/17) that rotted repeatedly. Now derive expectations from REGISTRY itself and, for the schema version, from the migrations directory, so they can't rot the same way again. - test_fire_tracker_phase4.py: two tests hardcoded a nonexistent deployment path (/opt/meshai/meshai/router.py) that matches no Dockerfile WORKDIR in this repo; resolve the module path via importlib.util.find_spec instead. - test_tombstone_broadcast.py: asserted fire severity == "immediate", which commit 2f677e85 deliberately downgraded to "priority" (to stop fire broadcasts bypassing the Grouper/cooldown during NATS backlog replay) without updating this test. - test_pipeline_grouper.py: test_immediate_severity_bypasses_grouper asserted an immediate-severity bypass that commit 85d48ce3 ("fix(fire): remove immediate-severity exemption from grouper + cooldown") DELETED on purpose -- fire events carry _severity_override="immediate", and the exemption left fire with no rate control at all in normal live operation. Re-adding the bypass would re-open that fire-spam hole on a public-safety mesh, so the test moves, not the source. Renamed + inverted to assert the real contract (all severities coalesce; only a missing group_key passes through). - test_tail_followups.py: dispatcher mock was missing dispatch_scheduled_fire_broadcast (a method added alongside the generic dispatch_scheduled_broadcast; test_reminders.py already mocks both). - test_tracking_v057.py: guard required an empty tracking-family adapter list in Environment.tsx, but the frontend has long grouped the pre-existing native satpass adapter under the "Tracking" display section (its own "satpass" backend toggle, unrelated to the Phase-7 tracking family every other guard in this file confirms is still unimplemented). Narrowed the guard to allow only that known entry. - test_v052_dispatcher.py: two tests used category="wildfire_incident", which the phase3b fire migration (#33) forced onto a dedicated formatter via NATIVE_ALWAYS_DECIDE; swapped to wildfire_hotspot (same emoji/label, not in NATIVE_ALWAYS_DECIDE) to keep exercising the generic composer logic under test. Also fixes one stale COMMENT (comment-only, no logic change) in meshai/notifications/pipeline/__init__.py's start_pipeline(): it still claimed "Immediate events bypass the grouper and don't need this [periodic flush]", which has been false since 85d48ce3 and is precisely what makes the deleted bypass look like a missing feature. The comment now records that the removal was deliberate and must not be reverted. The remaining 6 failures are left untouched -- 2 confirmed real bugs, to be fixed deliberately in their own changes: - meshcore_transport.py defines `_resolve_contact` TWICE on MeshCoreTransport (line 171 from PR #56, line 1227 from PR #92). The second silently shadows the first, so the DM contact-resolution refetch-on-miss that #56 added is dead code in production. (3 tests) - SCHEMA_VERSION (persistence/db.py:33) is stale at 26 vs. the actual highest migration v28; v27 and v28 shipped without bumping it. (3 tests) Plus 1 environment gap, not a code defect: test_natural_language_fire_question_routes_to_llm needs the `openai` package, which is declared in requirements.txt but not installed here. Suite: 20 failed, 2240 passed, 72 skipped -> 7 failed, 2254 passed, 72 skipped. All 7 remaining failures are ones classified above; no new failures introduced elsewhere in the suite. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:36:19 -06:00
def test_immediate_severity_is_also_coalesced_no_bypass():
"""An immediate event WITH a group_key is held, like every other severity.
The grouper used to exempt severity == "immediate" from the coalescing
window. Commit 85d48ce3 ("fix(fire): remove immediate-severity exemption
from grouper + cooldown") DELETED that bypass on purpose: fire events
carry _severity_override="immediate", and the exemption meant they
skipped the coalescer and zeroed the dispatcher cooldown, leaving fire
with no rate control at all in normal live operation. Rate control now
applies to ALL severities; the drain-mode pacer covers reconnect bursts.
This test previously asserted the OLD bypass contract and had been red
ever since. Re-adding the bypass to make it pass would re-open the fire
broadcast-spam hole on a public-safety mesh -- so the test is what moves,
not the source.
"""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
g.handle(_ev("immediate"))
test: fix 13 stale tests in the red suite, leave 6 real-bug failures (#124) (#129) Triaged all 20 known-red tests. 13 were stale tests asserting rotted expectations against deliberate, documented behavior changes; fixed by deriving expected values instead of hard-coding, or updating the expectation to match a documented policy change: - test_adapter_config_foundation.py / test_adapter_config_api.py: REGISTRY/API key-count and key-set guards hard-coded magic numbers (59/94/17) that rotted repeatedly. Now derive expectations from REGISTRY itself and, for the schema version, from the migrations directory, so they can't rot the same way again. - test_fire_tracker_phase4.py: two tests hardcoded a nonexistent deployment path (/opt/meshai/meshai/router.py) that matches no Dockerfile WORKDIR in this repo; resolve the module path via importlib.util.find_spec instead. - test_tombstone_broadcast.py: asserted fire severity == "immediate", which commit 2f677e85 deliberately downgraded to "priority" (to stop fire broadcasts bypassing the Grouper/cooldown during NATS backlog replay) without updating this test. - test_pipeline_grouper.py: test_immediate_severity_bypasses_grouper asserted an immediate-severity bypass that commit 85d48ce3 ("fix(fire): remove immediate-severity exemption from grouper + cooldown") DELETED on purpose -- fire events carry _severity_override="immediate", and the exemption left fire with no rate control at all in normal live operation. Re-adding the bypass would re-open that fire-spam hole on a public-safety mesh, so the test moves, not the source. Renamed + inverted to assert the real contract (all severities coalesce; only a missing group_key passes through). - test_tail_followups.py: dispatcher mock was missing dispatch_scheduled_fire_broadcast (a method added alongside the generic dispatch_scheduled_broadcast; test_reminders.py already mocks both). - test_tracking_v057.py: guard required an empty tracking-family adapter list in Environment.tsx, but the frontend has long grouped the pre-existing native satpass adapter under the "Tracking" display section (its own "satpass" backend toggle, unrelated to the Phase-7 tracking family every other guard in this file confirms is still unimplemented). Narrowed the guard to allow only that known entry. - test_v052_dispatcher.py: two tests used category="wildfire_incident", which the phase3b fire migration (#33) forced onto a dedicated formatter via NATIVE_ALWAYS_DECIDE; swapped to wildfire_hotspot (same emoji/label, not in NATIVE_ALWAYS_DECIDE) to keep exercising the generic composer logic under test. Also fixes one stale COMMENT (comment-only, no logic change) in meshai/notifications/pipeline/__init__.py's start_pipeline(): it still claimed "Immediate events bypass the grouper and don't need this [periodic flush]", which has been false since 85d48ce3 and is precisely what makes the deleted bypass look like a missing feature. The comment now records that the removal was deliberate and must not be reverted. The remaining 6 failures are left untouched -- 2 confirmed real bugs, to be fixed deliberately in their own changes: - meshcore_transport.py defines `_resolve_contact` TWICE on MeshCoreTransport (line 171 from PR #56, line 1227 from PR #92). The second silently shadows the first, so the DM contact-resolution refetch-on-miss that #56 added is dead code in production. (3 tests) - SCHEMA_VERSION (persistence/db.py:33) is stale at 26 vs. the actual highest migration v28; v27 and v28 shipped without bumping it. (3 tests) Plus 1 environment gap, not a code defect: test_natural_language_fire_question_routes_to_llm needs the `openai` package, which is declared in requirements.txt but not installed here. Suite: 20 failed, 2240 passed, 72 skipped -> 7 failed, 2254 passed, 72 skipped. All 7 remaining failures are ones classified above; no new failures introduced elsewhere in the suite. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:36:19 -06:00
# Held for coalescing, NOT delivered straight through.
assert rec.received == []
assert g.held_count() == 1
# The periodic flush (start_pipeline's _grouper_flush_loop) is what
# eventually delivers it, once the window expires.
g2 = Grouper(next_handler=rec.handle, window_seconds=0.0)
g2.handle(_ev("immediate", group_key="gk2"))
assert g2.tick() == 1
assert len(rec.received) == 1
assert rec.received[0].severity == "immediate"
test: fix 13 stale tests in the red suite, leave 6 real-bug failures (#124) (#129) Triaged all 20 known-red tests. 13 were stale tests asserting rotted expectations against deliberate, documented behavior changes; fixed by deriving expected values instead of hard-coding, or updating the expectation to match a documented policy change: - test_adapter_config_foundation.py / test_adapter_config_api.py: REGISTRY/API key-count and key-set guards hard-coded magic numbers (59/94/17) that rotted repeatedly. Now derive expectations from REGISTRY itself and, for the schema version, from the migrations directory, so they can't rot the same way again. - test_fire_tracker_phase4.py: two tests hardcoded a nonexistent deployment path (/opt/meshai/meshai/router.py) that matches no Dockerfile WORKDIR in this repo; resolve the module path via importlib.util.find_spec instead. - test_tombstone_broadcast.py: asserted fire severity == "immediate", which commit 2f677e85 deliberately downgraded to "priority" (to stop fire broadcasts bypassing the Grouper/cooldown during NATS backlog replay) without updating this test. - test_pipeline_grouper.py: test_immediate_severity_bypasses_grouper asserted an immediate-severity bypass that commit 85d48ce3 ("fix(fire): remove immediate-severity exemption from grouper + cooldown") DELETED on purpose -- fire events carry _severity_override="immediate", and the exemption left fire with no rate control at all in normal live operation. Re-adding the bypass would re-open that fire-spam hole on a public-safety mesh, so the test moves, not the source. Renamed + inverted to assert the real contract (all severities coalesce; only a missing group_key passes through). - test_tail_followups.py: dispatcher mock was missing dispatch_scheduled_fire_broadcast (a method added alongside the generic dispatch_scheduled_broadcast; test_reminders.py already mocks both). - test_tracking_v057.py: guard required an empty tracking-family adapter list in Environment.tsx, but the frontend has long grouped the pre-existing native satpass adapter under the "Tracking" display section (its own "satpass" backend toggle, unrelated to the Phase-7 tracking family every other guard in this file confirms is still unimplemented). Narrowed the guard to allow only that known entry. - test_v052_dispatcher.py: two tests used category="wildfire_incident", which the phase3b fire migration (#33) forced onto a dedicated formatter via NATIVE_ALWAYS_DECIDE; swapped to wildfire_hotspot (same emoji/label, not in NATIVE_ALWAYS_DECIDE) to keep exercising the generic composer logic under test. Also fixes one stale COMMENT (comment-only, no logic change) in meshai/notifications/pipeline/__init__.py's start_pipeline(): it still claimed "Immediate events bypass the grouper and don't need this [periodic flush]", which has been false since 85d48ce3 and is precisely what makes the deleted bypass look like a missing feature. The comment now records that the removal was deliberate and must not be reverted. The remaining 6 failures are left untouched -- 2 confirmed real bugs, to be fixed deliberately in their own changes: - meshcore_transport.py defines `_resolve_contact` TWICE on MeshCoreTransport (line 171 from PR #56, line 1227 from PR #92). The second silently shadows the first, so the DM contact-resolution refetch-on-miss that #56 added is dead code in production. (3 tests) - SCHEMA_VERSION (persistence/db.py:33) is stale at 26 vs. the actual highest migration v28; v27 and v28 shipped without bumping it. (3 tests) Plus 1 environment gap, not a code defect: test_natural_language_fire_question_routes_to_llm needs the `openai` package, which is declared in requirements.txt but not installed here. Suite: 20 failed, 2240 passed, 72 skipped -> 7 failed, 2254 passed, 72 skipped. All 7 remaining failures are ones classified above; no new failures introduced elsewhere in the suite. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:36:19 -06:00
def test_no_group_key_still_passes_through_immediately():
"""The ONE remaining bypass: an event with no group_key isn't coalesced
(there's nothing to coalesce it against)."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
g.handle(_ev("immediate", group_key=None))
assert len(rec.received) == 1
assert g.held_count() == 0
def test_periodic_flush_drains_routine():
"""A routine event is held, then released by tick() once its window passes."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=0.0) # 0s window -> tick drains now
g.handle(_ev("routine"))
# Held on arrival, not yet delivered.
assert g.held_count() == 1
assert rec.received == []
# The periodic flush task calls tick(); simulate one tick.
drained = g.tick()
assert drained == 1
assert len(rec.received) == 1
assert rec.received[0].severity == "routine"
assert g.held_count() == 0
def test_priority_is_also_coalesced_not_bypassed():
"""Priority events still buffer (only immediate bypasses)."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
g.handle(_ev("priority"))
assert rec.received == []
assert g.held_count() == 1
fix(pipeline): let wildfire_spotting skip the grouper (category-scoped) (#131) wildfire_spotting is the most urgent signal in the system (a fire throwing embers past its own containment line), but every fire event carries a group_key (= event_id), so the Grouper held spotting for the full grouper_window_seconds (60s live) before it could even reach the dispatcher -- a ~60s latency FLOOR, and ~120s+ once FirePacer queuing is added on top. Add a module-level _NEVER_COALESCE_CATEGORIES frozenset and bypass the coalescing window for the categories in it. Currently: wildfire_spotting only. This is safe because spotting is already rate-limited AT THE SOURCE: a per-fire 1h cooldown (adapter_config.fires.spotting_cooldown_seconds, default 3600, latched on fires.last_spotting_broadcast_at per irwin_id in gating/firms.py) gates spotting DETECTION itself, so N active fires yield at most N spotting alerts per hour. FirePacer (60s interval, with head-of-line ordering for immediate severity) and the dispatcher's per-(toggle, category, region) cooldown still apply downstream. The bypass is scoped by CATEGORY, never by severity. Commit 85d48ce3 deliberately removed a severity == "immediate" bypass from this exact spot because ALL fire events carry _severity_override="immediate", so a severity bypass exempts the entire fire family from rate control. The comment on the constant spells that out so it does not get re-added. Tests: spotting with a group_key passes straight through; wildfire_growth and wildfire_incident at immediate severity are STILL held (proving no severity bypass crept back in); spotting with no group_key still passes through. PR #129's test_immediate_severity_is_also_coalesced_no_bypass is untouched and still passes. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:37:07 -06:00
def test_wildfire_spotting_bypasses_the_grouper_even_with_group_key():
"""wildfire_spotting is a CATEGORY-scoped bypass (owner-approved): it
skips the coalescing window entirely, even though it carries a
group_key that would otherwise hold it. This is the one narrow
exemption -- see _NEVER_COALESCE_CATEGORIES in grouper.py for why
it's safe (source-side 1h per-fire cooldown) and why it must stay
category-scoped, not severity-scoped."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
ev = make_event(
source="firms",
category="wildfire_spotting",
severity="immediate",
title="test spotting",
lat=42.6,
lon=-114.5,
group_key="fire-irwin-123",
)
g.handle(ev)
# Passed straight through -- NOT held for the coalescing window.
assert len(rec.received) == 1
assert rec.received[0].category == "wildfire_spotting"
assert g.held_count() == 0
def test_wildfire_growth_immediate_is_still_held_no_severity_bypass_crept_in():
"""A same-severity, same-fire-family event of a DIFFERENT category
(wildfire_growth, not wildfire_spotting) must still be coalesced.
This proves the new bypass is scoped to category and did not
accidentally reintroduce a severity-based bypass (the exact bug
commit 85d48ce3 removed)."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
ev = make_event(
source="wfigs",
category="wildfire_growth",
severity="immediate",
title="test growth",
lat=42.6,
lon=-114.5,
group_key="fire-irwin-123",
)
g.handle(ev)
assert rec.received == []
assert g.held_count() == 1
def test_wildfire_incident_immediate_is_still_held_no_severity_bypass_crept_in():
"""Same as above for wildfire_incident: only wildfire_spotting bypasses,
every other fire category (even at immediate severity) is coalesced."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
ev = make_event(
source="wfigs",
category="wildfire_incident",
severity="immediate",
title="test incident",
lat=42.6,
lon=-114.5,
group_key="fire-irwin-123",
)
g.handle(ev)
assert rec.received == []
assert g.held_count() == 1
def test_wildfire_spotting_with_no_group_key_still_passes_through():
"""Existing no-group_key behavior is preserved for spotting too --
the bypass condition is an `or`, not a replacement."""
rec = Recorder()
g = Grouper(next_handler=rec.handle, window_seconds=60.0)
ev = make_event(
source="firms",
category="wildfire_spotting",
severity="immediate",
title="test spotting no key",
lat=42.6,
lon=-114.5,
group_key=None,
)
g.handle(ev)
assert len(rec.received) == 1
assert g.held_count() == 0