"""Tests for broadcast_versions per-subsystem contract (daemon.server). broadcast_versions(subsystem) sends exactly one data-carrying message for its subsystem — no legacy `updated` field — and skips the broadcast entirely when the subsystem's state is None (collector failed). """ import asyncio import json from unittest.mock import AsyncMock, MagicMock, patch import daemon.server as server class TestBroadcastVersionsPerSubsystem: def _ws(self): ws = AsyncMock() ws.send_str = AsyncMock() server._ws_subscribers.add(ws) return ws def test_only_target_subsystem_sent(self): """Each subscriber gets one versions message carrying that subsystem.""" store = MagicMock() store.get.side_effect = lambda name: {"v": name} ws = self._ws() try: with patch.object(server, "state_store", store): asyncio.run(server.broadcast_versions("firewall")) ws.send_str.assert_awaited_once() msg = json.loads(ws.send_str.call_args[0][0]) assert msg == { "type": "versions", "subsystem": "firewall", "data": {"v": "firewall"}, } finally: server._ws_subscribers.discard(ws) def test_data_per_subsystem_not_shared(self): """The data payload is that subsystem's state, not another's.""" store = MagicMock() store.get.side_effect = lambda name: {"name": name} ws = self._ws() try: with patch.object(server, "state_store", store): asyncio.run(server.broadcast_versions("dnsmasq")) asyncio.run(server.broadcast_versions("acme")) msgs = [json.loads(c[0][0]) for c in ws.send_str.call_args_list] assert [(m["subsystem"], m["data"]) for m in msgs] == [ ("dnsmasq", {"name": "dnsmasq"}), ("acme", {"name": "acme"}), ] # No legacy diff field in any message. for m in msgs: assert "updated" not in m finally: server._ws_subscribers.discard(ws) def test_none_state_produces_no_message(self): """A None payload (failed collection) is skipped — no clobber.""" store = MagicMock() store.get.return_value = None ws = self._ws() try: with patch.object(server, "state_store", store): asyncio.run(server.broadcast_versions("firewall")) ws.send_str.assert_not_awaited() finally: server._ws_subscribers.discard(ws) def test_no_bump_called(self): """broadcast_versions never bumps — callers own the version counter.""" store = MagicMock() store.get.return_value = {"a": 1} store.bump = MagicMock() ws = self._ws() try: with patch.object(server, "state_store", store): asyncio.run(server.broadcast_versions("firewall")) store.bump.assert_not_called() finally: server._ws_subscribers.discard(ws) def test_dead_subscriber_removed(self): """A failing subscriber is pruned and healthy ones still receive data.""" store = MagicMock() store.get.return_value = {"a": 1} dead = AsyncMock() dead.send_str = AsyncMock(side_effect=Exception("broken")) healthy = self._ws() server._ws_subscribers.add(dead) try: with patch.object(server, "state_store", store): asyncio.run(server.broadcast_versions("firewall")) assert dead not in server._ws_subscribers healthy.send_str.assert_awaited_once() finally: server._ws_subscribers.discard(dead) server._ws_subscribers.discard(healthy)