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
183
work/tests/test_pipeline_skeleton.py
Normal file
183
work/tests/test_pipeline_skeleton.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"""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()
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue