refactor(bus): extract publish_safely + extend topics for DEBT-031
Shared publish_safely helper at decnet/bus/publish.py so the nine workers about to be wired into the bus don't each copy-paste the "never raise back at the caller" contract. Mutator drops its private copy and imports the canonical one. topics.py gains the attacker.* hierarchy (observed, scored, session.started, session.ended) and a system_health(worker) builder for per-worker health heartbeats — both prerequisites for the worker rollout under DEBT-031.
This commit is contained in:
64
tests/bus/test_publish.py
Normal file
64
tests/bus/test_publish.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Tests for :mod:`decnet.bus.publish`.
|
||||
|
||||
The whole point of ``publish_safely`` is that it never raises back at the
|
||||
caller. These tests pin that contract: ``None`` bus is a no-op, a real
|
||||
bus publishes, and a raising bus is swallowed + logged.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from decnet.bus.base import BaseBus, Event, Subscription
|
||||
from decnet.bus.fake import FakeBus
|
||||
from decnet.bus.publish import publish_safely
|
||||
|
||||
|
||||
class _ExplodingBus(BaseBus):
|
||||
"""Minimal bus whose ``publish`` always raises."""
|
||||
|
||||
async def connect(self) -> None: # pragma: no cover - trivial
|
||||
return None
|
||||
|
||||
async def publish(self, topic, payload, *, event_type=""):
|
||||
raise RuntimeError("transport exploded")
|
||||
|
||||
def subscribe(self, pattern: str) -> Subscription: # pragma: no cover
|
||||
raise NotImplementedError
|
||||
|
||||
async def close(self) -> None: # pragma: no cover - trivial
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_safely_none_bus_is_noop() -> None:
|
||||
# Must not raise. A worker that couldn't connect at startup passes
|
||||
# bus=None and expects every call to silently no-op.
|
||||
await publish_safely(None, "system.log", {"msg": "hi"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_safely_delivers_on_live_bus() -> None:
|
||||
bus = FakeBus()
|
||||
await bus.connect()
|
||||
try:
|
||||
sub = bus.subscribe("system.log")
|
||||
async with sub:
|
||||
await publish_safely(bus, "system.log", {"msg": "hi"}, event_type="log")
|
||||
event = await sub.__anext__()
|
||||
assert isinstance(event, Event)
|
||||
assert event.topic == "system.log"
|
||||
assert event.type == "log"
|
||||
assert event.payload == {"msg": "hi"}
|
||||
finally:
|
||||
await bus.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_safely_swallows_transport_errors(caplog: pytest.LogCaptureFixture) -> None:
|
||||
caplog.set_level(logging.WARNING, logger="bus.publish")
|
||||
# The exploding bus would crash the caller without publish_safely.
|
||||
# After wrapping, the caller sees nothing but a log line.
|
||||
await publish_safely(_ExplodingBus(), "system.log", {"msg": "hi"})
|
||||
assert any("bus publish failed" in rec.message for rec in caplog.records)
|
||||
@@ -40,3 +40,23 @@ def test_segment_validation(bad: str) -> None:
|
||||
topics.topology_status(bad)
|
||||
with pytest.raises(ValueError):
|
||||
topics.decky(bad, topics.DECKY_STATE)
|
||||
with pytest.raises(ValueError):
|
||||
topics.system_health(bad)
|
||||
|
||||
|
||||
def test_attacker_builder() -> None:
|
||||
assert topics.attacker(topics.ATTACKER_OBSERVED) == "attacker.observed"
|
||||
assert topics.attacker(topics.ATTACKER_SCORED) == "attacker.scored"
|
||||
# Dotted leaf is intentional — same as system.bus.health.
|
||||
assert topics.attacker(topics.ATTACKER_SESSION_STARTED) == "attacker.session.started"
|
||||
assert topics.attacker(topics.ATTACKER_SESSION_ENDED) == "attacker.session.ended"
|
||||
|
||||
|
||||
def test_attacker_builder_rejects_empty() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
topics.attacker("")
|
||||
|
||||
|
||||
def test_system_health_builder() -> None:
|
||||
assert topics.system_health("sniffer") == "system.sniffer.health"
|
||||
assert topics.system_health("mutator") == "system.mutator.health"
|
||||
|
||||
Reference in New Issue
Block a user