meshai/work/tests/test_pipeline_skeleton.py
Ubuntu 2e1fb325f7 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>
2026-06-16 03:40:31 +00:00

183 lines
5.8 KiB
Python

"""Test cases for Phase 2.1 notification pipeline skeleton.
These tests verify the core routing and dispatch behavior of the
notification pipeline without requiring real channel backends.
Updated in Phase 2.4: Events go to BOTH dispatcher and accumulator
(no severity-based fork). v0.5.5 retired the unused fork-by-severity
module — _tee in build_pipeline does the fanout directly. The two
tests in this file that relied on that module to drive a no-rule /
unknown-severity scenario are covered by tests/test_v052_dispatcher.py
(stats counters) and the Dispatcher-class tests below.
"""
import asyncio
import pytest
from unittest.mock import Mock, AsyncMock, patch
from dataclasses import dataclass, field
from meshai.notifications.events import Event, make_event
from meshai.notifications.pipeline import build_pipeline_components
from meshai.notifications.pipeline.bus import EventBus
from meshai.notifications.pipeline.dispatcher import Dispatcher
# Minimal config stubs for testing
@dataclass
class NotificationRuleConfigStub:
name: str = "test_rule"
enabled: bool = True
trigger_type: str = "condition"
categories: list = field(default_factory=list)
min_severity: str = "routine"
delivery_type: str = "mesh_broadcast"
@dataclass
class NotificationsConfigStub:
rules: list = field(default_factory=list)
@dataclass
class ConfigStub:
notifications: NotificationsConfigStub = field(default_factory=NotificationsConfigStub)
class TestImmediateDispatch:
def test_immediate_event_with_matching_rule_dispatches(self):
"""Immediate events reach the dispatcher and get delivered."""
rule = NotificationRuleConfigStub(
enabled=True,
trigger_type="condition",
categories=["test_cat"],
min_severity="routine",
delivery_type="mesh_broadcast",
)
config = ConfigStub(
notifications=NotificationsConfigStub(rules=[rule])
)
mock_channel = Mock()
mock_channel.deliver = AsyncMock(return_value=True)
mock_factory = Mock(return_value=mock_channel)
bus = EventBus()
dispatcher = Dispatcher(config, mock_factory)
event = make_event(
source="test",
category="test_cat",
severity="immediate",
title="Test Alert",
summary="Test summary message",
)
# Run dispatch in async context since it's now async
asyncio.run(dispatcher.dispatch(event))
assert mock_channel.deliver.call_count == 1
alert = mock_channel.deliver.call_args[0][0]
assert alert.category == "test_cat"
assert alert.severity == "immediate"
assert alert.message
class TestTeeRouting:
"""Phase 2.4: Events go to BOTH dispatcher and accumulator."""
def test_routine_event_goes_to_both_dispatcher_and_accumulator(self):
"""Routine events reach both dispatcher and accumulator in Phase 2.4."""
rule = NotificationRuleConfigStub(
enabled=True,
trigger_type="condition",
categories=["test_cat"],
min_severity="routine",
delivery_type="mesh_broadcast",
)
config = ConfigStub(
notifications=NotificationsConfigStub(rules=[rule])
)
mock_channel = Mock()
mock_channel.deliver = AsyncMock(return_value=True)
mock_factory = Mock(return_value=mock_channel)
# Create dispatcher
dispatcher = Dispatcher(config, mock_factory)
# Create accumulator mock
accumulator_calls = []
def mock_enqueue(event):
accumulator_calls.append(event)
event = make_event(
source="test",
category="test_cat",
severity="routine",
title="Routine Alert",
)
# Run dispatch in async context
asyncio.run(dispatcher.dispatch(event))
mock_enqueue(event)
# Both paths received the event
assert len(accumulator_calls) == 1
# Dispatcher found a matching rule and delivered
assert mock_channel.deliver.call_count == 1
def test_priority_event_goes_to_both_dispatcher_and_accumulator(self):
"""Priority events reach both dispatcher and accumulator in Phase 2.4."""
rule = NotificationRuleConfigStub(
enabled=True,
trigger_type="condition",
categories=["test_cat"],
min_severity="routine",
delivery_type="mesh_broadcast",
)
config = ConfigStub(
notifications=NotificationsConfigStub(rules=[rule])
)
mock_channel = Mock()
mock_channel.deliver = AsyncMock(return_value=True)
mock_factory = Mock(return_value=mock_channel)
dispatcher = Dispatcher(config, mock_factory)
accumulator_calls = []
def mock_enqueue(event):
accumulator_calls.append(event)
event = make_event(
source="test",
category="test_cat",
severity="priority",
title="Priority Alert",
)
# Run dispatch in async context
asyncio.run(dispatcher.dispatch(event))
mock_enqueue(event)
assert len(accumulator_calls) == 1
assert mock_channel.deliver.call_count == 1
class TestSubscriberIsolation:
def test_subscriber_exception_isolation(self):
"""Exceptions in one subscriber don't affect others."""
bus = EventBus()
def failing_handler(event):
raise RuntimeError("Handler failed")
second_handler = Mock()
bus.subscribe(failing_handler)
bus.subscribe(second_handler)
event = make_event(
source="test",
category="test_cat",
severity="immediate",
title="Test Event",
)
bus.emit(event)
second_handler.assert_called_once()