import asyncio import json from unittest.mock import AsyncMock, MagicMock import pytest from server.streaming import StreamManager @pytest.fixture def manager(): return StreamManager() @pytest.fixture def mock_ws(): ws = AsyncMock() ws.send_text = AsyncMock() return ws class TestClientManagement: def test_add_client(self, manager, mock_ws): """Adding a client increases count.""" manager.add_client(mock_ws) assert manager.client_count == 1 def test_remove_client(self, manager, mock_ws): """Removing a client decreases count.""" manager.add_client(mock_ws) manager.remove_client(mock_ws) assert manager.client_count == 0 def test_remove_missing_client(self, manager, mock_ws): """Removing a non-existent client doesn't error.""" manager.remove_client(mock_ws) assert manager.client_count == 0 class TestBroadcast: @pytest.mark.asyncio async def test_broadcast_phase(self, manager, mock_ws): """Phase update is broadcast to all clients.""" manager.add_client(mock_ws) await manager.broadcast_phase( intensity=2.4, params={"morph_speed": 0.35, "shader_severity": 0.6, "palette": "crimson_void"}, ) mock_ws.send_text.assert_called_once() msg = json.loads(mock_ws.send_text.call_args[0][0]) assert msg["type"] == "phase" assert msg["intensity"] == 2.4 assert msg["params"]["morph_speed"] == 0.35 @pytest.mark.asyncio async def test_broadcast_asset(self, manager, mock_ws): """Asset notification is broadcast.""" manager.add_client(mock_ws) await manager.broadcast_asset( url="/assets/img/abc.png", severity=1.8, transition="melt", ) msg = json.loads(mock_ws.send_text.call_args[0][0]) assert msg["type"] == "asset" assert msg["url"] == "/assets/img/abc.png" @pytest.mark.asyncio async def test_broadcast_whisper(self, manager, mock_ws): """Whisper notification is broadcast.""" manager.add_client(mock_ws) await manager.broadcast_whisper( url="/assets/audio/w01.wav", pan=-0.3, volume=0.4, reverb=0.7, ) msg = json.loads(mock_ws.send_text.call_args[0][0]) assert msg["type"] == "whisper" @pytest.mark.asyncio async def test_broadcast_scare(self, manager, mock_ws): """Scare event is broadcast.""" manager.add_client(mock_ws) await manager.broadcast_scare(effect="face_flash", duration_ms=150) msg = json.loads(mock_ws.send_text.call_args[0][0]) assert msg["type"] == "scare" assert msg["effect"] == "face_flash" @pytest.mark.asyncio async def test_dead_client_cleanup(self, manager): """Dead clients are removed on broadcast.""" dead_ws = AsyncMock() dead_ws.send_text = AsyncMock(side_effect=Exception("connection closed")) manager.add_client(dead_ws) assert manager.client_count == 1 await manager.broadcast_phase(intensity=1.0, params={}) assert manager.client_count == 0