mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
refactor: move source tree into work/, multi-stage Docker build, fix satpass
- Move all application source (meshai/, dashboard-frontend/, tests/, config/, docs/, Dockerfile, etc.) into work/ directory - Add Node.js multi-stage build to Dockerfile for frontend compilation; remove compiled static assets from git tracking - Fix satpass missing time windows: consolidation was splitting wire on newline and only putting line 1 in event.title, dropping the time window line that the composer uses for precomposed broadcasts - Fix satpass burst flooding: stagger consolidation timers (+60s per pending pass) so Central batch publishes don't blast the mesh - Update CI workflow build context to work/ - Anchor lib/ and data/ gitignore patterns to repo root to prevent false matches on nested directories - Add dashboard-frontend/node_modules/ to .dockerignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7128b432ee
commit
2e1fb325f7
283 changed files with 109402 additions and 483 deletions
194
work/tests/test_pipeline_inhibitor_grouper.py
Normal file
194
work/tests/test_pipeline_inhibitor_grouper.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"""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", "priority", 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", "priority", group_key="fire:42")
|
||||
ev2 = make_event("e2", "priority", 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", "priority", 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", "priority", group_key="g1")
|
||||
ev2 = make_event("e2", "priority", group_key="g2")
|
||||
ev3 = make_event("e3", "priority", 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 priority event with group_key and inhibit_keys (immediate would bypass)
|
||||
ev1 = make_event("e1", "priority", 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 priority event, not the suppressed routine
|
||||
Loading…
Add table
Add a link
Reference in a new issue