meshai/tests/test_pipeline_inhibitor_grouper.py

194 lines
6.8 KiB
Python
Raw Normal View History

"""Tests for Phase 2.2 inhibitor and grouper."""
import pytest
from unittest.mock import Mock
from meshai.notifications.events import Event
from meshai.notifications.pipeline.inhibitor import Inhibitor
from meshai.notifications.pipeline.grouper import Grouper
def make_event(id, severity, inhibit_keys=None, group_key=None):
return Event(
id=id,
source="test",
category="test_cat",
severity=severity,
title=f"Event {id}",
inhibit_keys=inhibit_keys or [],
group_key=group_key,
)
# ===================== INHIBITOR TESTS =====================
class TestInhibitor:
def test_event_without_inhibit_keys_passes_through(self):
next_handler = Mock()
inhibitor = Inhibitor(next_handler)
event = make_event("e1", "immediate", inhibit_keys=[])
inhibitor.handle(event)
next_handler.assert_called_once_with(event)
def test_lower_severity_after_higher_is_suppressed(self):
next_handler = Mock()
inhibitor = Inhibitor(next_handler)
ev1 = make_event("e1", "immediate", inhibit_keys=["battery:NODE1"])
ev2 = make_event("e2", "priority", inhibit_keys=["battery:NODE1"])
inhibitor.handle(ev1)
inhibitor.handle(ev2)
assert next_handler.call_count == 1
next_handler.assert_called_once_with(ev1)
def test_equal_severity_is_suppressed(self):
next_handler = Mock()
inhibitor = Inhibitor(next_handler)
ev1 = make_event("e1", "priority", inhibit_keys=["key1"])
ev2 = make_event("e2", "priority", inhibit_keys=["key1"])
inhibitor.handle(ev1)
inhibitor.handle(ev2)
assert next_handler.call_count == 1
def test_higher_severity_after_lower_passes_and_upgrades(self):
next_handler = Mock()
inhibitor = Inhibitor(next_handler)
ev1 = make_event("e1", "priority", inhibit_keys=["key1"])
ev2 = make_event("e2", "immediate", inhibit_keys=["key1"])
inhibitor.handle(ev1)
inhibitor.handle(ev2)
assert next_handler.call_count == 2
keys = inhibitor.active_keys()
assert keys["key1"][0] == 2 # immediate rank
def test_inhibit_key_expires_after_ttl(self):
next_handler = Mock()
inhibitor = Inhibitor(next_handler, ttl_seconds=10)
current_time = [0.0]
inhibitor._now = lambda: current_time[0]
ev1 = make_event("e1", "immediate", inhibit_keys=["key1"])
current_time[0] = 0.0
inhibitor.handle(ev1)
current_time[0] = 15.0
ev2 = make_event("e2", "routine", inhibit_keys=["key1"])
inhibitor.handle(ev2)
assert next_handler.call_count == 2
def test_multiple_keys_any_active_suppresses(self):
next_handler = Mock()
inhibitor = Inhibitor(next_handler)
ev1 = make_event("e1", "immediate", inhibit_keys=["a", "b"])
inhibitor.handle(ev1)
assert next_handler.call_count == 1
ev2 = make_event("e2", "routine", inhibit_keys=["b", "c"])
inhibitor.handle(ev2)
assert next_handler.call_count == 1 # suppressed by "b"
ev3 = make_event("e3", "routine", inhibit_keys=["c", "d"])
inhibitor.handle(ev3)
assert next_handler.call_count == 2 # passes, no active key
# ===================== GROUPER TESTS =====================
class TestGrouper:
def test_event_without_group_key_emits_immediately(self):
next_handler = Mock()
grouper = Grouper(next_handler)
event = make_event("e1", "immediate", group_key=None)
grouper.handle(event)
next_handler.assert_called_once_with(event)
def test_event_with_group_key_is_held_not_emitted(self):
next_handler = Mock()
grouper = Grouper(next_handler)
event = make_event("e1", "immediate", group_key="fire:42")
grouper.handle(event)
next_handler.assert_not_called()
assert grouper.held_count() == 1
def test_second_same_group_key_replaces_first(self):
next_handler = Mock()
grouper = Grouper(next_handler)
ev1 = make_event("e1", "immediate", group_key="fire:42")
ev2 = make_event("e2", "immediate", group_key="fire:42")
grouper.handle(ev1)
grouper.handle(ev2)
next_handler.assert_not_called()
assert grouper.held_count() == 1
grouper.flush_all()
assert next_handler.call_count == 1
emitted_event = next_handler.call_args[0][0]
assert emitted_event.id == "e2"
def test_tick_emits_when_window_expired(self):
next_handler = Mock()
grouper = Grouper(next_handler, window_seconds=5)
current_time = [0.0]
grouper._now = lambda: current_time[0]
current_time[0] = 0.0
event = make_event("e1", "immediate", group_key="g")
grouper.handle(event)
assert grouper.held_count() == 1
current_time[0] = 3.0
grouper.tick()
next_handler.assert_not_called()
assert grouper.held_count() == 1
current_time[0] = 10.0
grouper.tick()
next_handler.assert_called_once()
assert grouper.held_count() == 0
def test_flush_all_emits_everything_immediately(self):
next_handler = Mock()
grouper = Grouper(next_handler)
ev1 = make_event("e1", "immediate", group_key="g1")
ev2 = make_event("e2", "immediate", group_key="g2")
ev3 = make_event("e3", "immediate", group_key="g3")
grouper.handle(ev1)
grouper.handle(ev2)
grouper.handle(ev3)
assert grouper.held_count() == 3
grouper.flush_all()
assert next_handler.call_count == 3
assert grouper.held_count() == 0
# ===================== INTEGRATION TEST =====================
class TestInhibitorGrouperChain:
def test_inhibitor_then_grouper_chain(self):
terminal = Mock()
grouper = Grouper(next_handler=terminal)
inhibitor = Inhibitor(next_handler=grouper.handle)
# Send immediate event with group_key and inhibit_keys
ev1 = make_event("e1", "immediate", group_key="g1", inhibit_keys=["k1"])
inhibitor.handle(ev1)
# After inhibitor: passed (no prior key)
# After grouper: held (group_key present)
terminal.assert_not_called()
assert grouper.held_count() == 1
# Send routine event with same group_key and inhibit_keys
ev2 = make_event("e2", "routine", group_key="g1", inhibit_keys=["k1"])
inhibitor.handle(ev2)
# After inhibitor: SUPPRESSED (k1 active at higher rank)
terminal.assert_not_called()
assert grouper.held_count() == 1 # still 1, not 2
# Flush grouper
grouper.flush_all()
terminal.assert_called_once()
emitted = terminal.call_args[0][0]
assert emitted.id == "e1" # the immediate, not suppressed routine