"""Tests for WS delta structure (daemon.server._poll_loop + broadcasts). After the push-stream migration the poll loop drives per-subsystem deltas: a structural diff bumps the version and broadcasts {type: versions, subsystem, data}; a volatile-only diff broadcasts {type: tick, subsystem, data}. No legacy `updated` dict / `subsystems` array is emitted. """ import asyncio import hashlib import json from contextlib import suppress from unittest.mock import AsyncMock, MagicMock, patch import daemon.server as server from lib.state import State def _zero_offset_subsystem(interval: int) -> str: """Find a subsystem name whose md5 offset is 0 so the loop starts at once.""" for i in range(100_000): name = f"sub{i}" offset = int(hashlib.md5(name.encode()).hexdigest(), 16) % interval if offset == 0: return name raise AssertionError("could not find zero-offset name") def _run_one_poll_iteration(poll_result): """Run _poll_loop for a single iteration and return the broadcast mocks.""" namespaced = _zero_offset_subsystem(60) async def drive(): store = MagicMock() store.poll.return_value = poll_result store.bump = MagicMock() store.get.return_value = {"value": 1} bv = AsyncMock() bt = AsyncMock() task = None with ( patch.object(server, "state_store", store), patch.object(server, "broadcast_versions", bv), patch.object(server, "broadcast_tick", bt), patch.object(server, "blacklist_expired"), ): task = asyncio.create_task(server._poll_loop(namespaced, 60)) await asyncio.sleep(0.02) # let one full iteration run task.cancel() with suppress(asyncio.CancelledError): await task return store, bv, bt store, bv, bt = asyncio.run(drive()) return store, bv, bt class TestPollLoopDeltas: def test_structural_change_broadcasts_versions(self): store, bv, bt = _run_one_poll_iteration((True, False)) store.bump.assert_called_once_with(_zero_offset_subsystem(60)) bv.assert_awaited_once() bt.assert_not_awaited() def test_volatile_change_broadcasts_tick(self): store, bv, bt = _run_one_poll_iteration((False, True)) store.bump.assert_not_called() bv.assert_not_awaited() bt.assert_awaited_once() def test_no_change_no_broadcast(self): store, bv, bt = _run_one_poll_iteration((False, False)) store.bump.assert_not_called() bv.assert_not_awaited() bt.assert_not_awaited() class TestDeltaMessageShape: def test_versions_message_carries_subsystem_and_data(self): """broadcast_versions emits {type, subsystem, data} — no `updated`.""" store = State() store.set("firewall", {"zones": {"public": {}}}) ws = AsyncMock() ws.send_str = AsyncMock() server._ws_subscribers.add(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" assert msg["subsystem"] == "firewall" assert msg["data"] == {"zones": {"public": {}}} assert "updated" not in msg finally: server._ws_subscribers.discard(ws) def test_tick_message_carries_subsystem_and_data(self): store = State() store.set("system", {"load": {"load1": 1.0}}) ws = AsyncMock() ws.send_str = AsyncMock() server._ws_subscribers.add(ws) try: with patch.object(server, "state_store", store): asyncio.run(server.broadcast_tick("system")) ws.send_str.assert_awaited_once() msg = json.loads(ws.send_str.call_args[0][0]) assert msg["type"] == "tick" assert msg["subsystem"] == "system" assert msg["data"] == {"load": {"load1": 1.0}} assert "subsystems" not in msg finally: server._ws_subscribers.discard(ws)