feat(notifications): Phase 2.5a channel interface unification

- Switch channels.py from dict-based to dataclass-based interfaces
- Add NotificationPayload dataclass and make_payload_from_event helper
- Update channel.deliver() to be async with (payload, rule) signature
- Add connector parameter to Dispatcher, DigestScheduler, and pipeline builders
- Update pipeline tee to use asyncio.create_task for async dispatch
- Add create_channel_from_dict for legacy router.py compatibility
- Update tests for new async interfaces

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-15 03:45:27 +00:00
commit c9d9a9925c
8 changed files with 235 additions and 129 deletions

View file

@ -60,8 +60,9 @@ class MockChannel:
def __init__(self):
self.deliveries = []
def deliver(self, payload: dict):
async def deliver(self, payload, rule=None):
self.deliveries.append(payload)
return True
class MockLLMBackend:
@ -93,7 +94,7 @@ def make_scheduler(
channels = {}
def channel_factory(rule):
def channel_factory(rule, connector=None):
ch = MockChannel()
channels[rule.name] = ch
return ch
@ -223,8 +224,8 @@ class TestFireBehavior:
ch = channels["digest-mesh"]
assert len(ch.deliveries) == 1
payload = ch.deliveries[0]
assert payload["category"] == "digest"
assert payload["severity"] == "routine"
assert payload.category == "digest"
assert payload.severity == "routine"
def test_fire_skips_disabled_rules(self):
"""Disabled rules are not delivered to."""
@ -293,8 +294,8 @@ class TestFireBehavior:
ch = channels["mesh"]
assert len(ch.deliveries) >= 1
for payload in ch.deliveries:
assert "chunk_index" in payload
assert "chunk_total" in payload
assert payload.chunk_index is not None
assert payload.chunk_total is not None
def test_fire_email_delivery_full_text(self):
"""Email delivery type gets single full-text delivery."""
@ -320,8 +321,8 @@ class TestFireBehavior:
ch = channels["email"]
assert len(ch.deliveries) == 1
payload = ch.deliveries[0]
assert "chunk_index" not in payload
assert "--- " in payload["message"]
assert payload.chunk_index is None
assert "--- " in payload.message
def test_fire_updates_last_fire_at(self):
"""_fire() updates last_fire_at timestamp."""
@ -350,7 +351,7 @@ class TestFireBehavior:
ch = channels["mesh"]
assert len(ch.deliveries) == 1
assert "No alerts" in ch.deliveries[0]["message"]
assert "No alerts" in ch.deliveries[0].message
# ---- Lifecycle Tests ----
@ -520,11 +521,11 @@ class TestIntegration:
call_order = []
def bad_channel_factory(rule):
def bad_channel_factory(rule, connector=None):
call_order.append(rule.name)
if rule.name == "bad":
ch = MagicMock()
ch.deliver.side_effect = RuntimeError("delivery failed")
ch.deliver = AsyncMock(side_effect=RuntimeError("delivery failed"))
return ch
return MockChannel()