""" Tests for WebSocket substitution handlers. Covers all three substitution types: - Pinch hitter (batting substitution) - Defensive replacement (field substitution) - Pitching change (pitcher substitution) Each handler follows the same pattern: validate input, acquire lock, call SubstitutionManager, broadcast results. Author: Claude Date: 2025-01-27 """ import asyncio import pytest from contextlib import asynccontextmanager from uuid import uuid4 from unittest.mock import AsyncMock, MagicMock, patch from app.core.substitution_manager import SubstitutionResult # ============================================================================ # TEST FIXTURES # ============================================================================ @pytest.fixture def mock_sub_result_success(): """Create a successful substitution result.""" return SubstitutionResult( success=True, player_out_lineup_id=1, player_in_card_id=999, new_lineup_id=100, new_position="CF", new_batting_order=1, error_message=None, error_code=None, ) @pytest.fixture def mock_sub_result_failure(): """Create a failed substitution result.""" return SubstitutionResult( success=False, player_out_lineup_id=1, player_in_card_id=999, new_lineup_id=None, new_position=None, new_batting_order=None, error_message="Player not found in roster", error_code="PLAYER_NOT_IN_ROSTER", ) @pytest.fixture def base_sub_data(): """Base data for substitution requests.""" return { "game_id": str(uuid4()), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, } # ============================================================================ # PINCH HITTER TESTS # ============================================================================ class TestPinchHitter: """Tests for request_pinch_hitter handler.""" @pytest.mark.asyncio async def test_pinch_hitter_success( self, mock_manager, mock_game_state, mock_sub_result_success ): """ Verify successful pinch hitter substitution. Should call SubstitutionManager.pinch_hit(), broadcast player_substituted, and confirm to requester. """ from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def mock_lock(game_id, timeout=30.0): yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = mock_lock mock_sub_instance = MagicMock() mock_sub_instance.pinch_hit = AsyncMock(return_value=mock_sub_result_success) MockSubMgr.return_value = mock_sub_instance from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) # Verify SubstitutionManager called mock_sub_instance.pinch_hit.assert_called_once() # Verify broadcast mock_manager.broadcast_to_game.assert_called() broadcast_call = mock_manager.broadcast_to_game.call_args assert broadcast_call[0][1] == "player_substituted" assert broadcast_call[0][2]["type"] == "pinch_hitter" # Verify confirmation to requester mock_manager.emit_to_user.assert_called() confirm_call = mock_manager.emit_to_user.call_args assert confirm_call[0][1] == "substitution_confirmed" @pytest.mark.asyncio async def test_pinch_hitter_missing_game_id(self, mock_manager): """Verify error when game_id is missing.""" from socketio import AsyncServer from app.websocket.handlers import register_handlers sio = AsyncServer() register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", {"player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1}, ) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "MISSING_FIELD" in call_args[0][2]["code"] @pytest.mark.asyncio async def test_pinch_hitter_invalid_game_id(self, mock_manager): """Verify error when game_id is invalid UUID.""" from socketio import AsyncServer from app.websocket.handlers import register_handlers sio = AsyncServer() register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": "not-a-uuid", "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "INVALID_FORMAT" in call_args[0][2]["code"] @pytest.mark.asyncio async def test_pinch_hitter_game_not_found(self, mock_manager): """Verify error when game doesn't exist.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = None from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(uuid4()), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert "not found" in call_args[0][2]["message"] @pytest.mark.asyncio async def test_pinch_hitter_missing_player_out(self, mock_manager, mock_game_state): """Verify error when player_out_lineup_id is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "player_out" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_pinch_hitter_missing_player_in(self, mock_manager, mock_game_state): """Verify error when player_in_card_id is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "player_in" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_pinch_hitter_missing_team_id(self, mock_manager, mock_game_state): """Verify error when team_id is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "team_id" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_pinch_hitter_validation_failure( self, mock_manager, mock_game_state, mock_sub_result_failure ): """Verify SubstitutionManager failure is propagated to user.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def mock_lock(game_id, timeout=30.0): yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = mock_lock mock_sub_instance = MagicMock() mock_sub_instance.pinch_hit = AsyncMock(return_value=mock_sub_result_failure) MockSubMgr.return_value = mock_sub_instance from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert call_args[0][2]["code"] == "PLAYER_NOT_IN_ROSTER" @pytest.mark.asyncio async def test_pinch_hitter_lock_timeout(self, mock_manager, mock_game_state): """Verify lock timeout returns server busy message.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def timeout_lock(game_id, timeout=30.0): raise asyncio.TimeoutError("Lock timeout") yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = timeout_lock from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pinch_hitter"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "error" assert "busy" in call_args[0][2]["message"].lower() # ============================================================================ # DEFENSIVE REPLACEMENT TESTS # ============================================================================ class TestDefensiveReplacement: """Tests for request_defensive_replacement handler.""" @pytest.mark.asyncio async def test_defensive_replacement_success( self, mock_manager, mock_game_state, mock_sub_result_success ): """Verify successful defensive replacement.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def mock_lock(game_id, timeout=30.0): yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = mock_lock mock_sub_instance = MagicMock() mock_sub_instance.defensive_replace = AsyncMock( return_value=mock_sub_result_success ) MockSubMgr.return_value = mock_sub_instance from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) # Verify SubstitutionManager called with position mock_sub_instance.defensive_replace.assert_called_once() call_kwargs = mock_sub_instance.defensive_replace.call_args[1] assert call_kwargs["new_position"] == "CF" # Verify broadcast type broadcast_call = mock_manager.broadcast_to_game.call_args assert broadcast_call[0][2]["type"] == "defensive_replacement" @pytest.mark.asyncio async def test_defensive_replacement_missing_game_id(self, mock_manager): """Verify error when game_id is missing.""" from socketio import AsyncServer from app.websocket.handlers import register_handlers sio = AsyncServer() register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "MISSING_FIELD" in call_args[0][2]["code"] @pytest.mark.asyncio async def test_defensive_replacement_invalid_game_id(self, mock_manager): """Verify error when game_id is invalid.""" from socketio import AsyncServer from app.websocket.handlers import register_handlers sio = AsyncServer() register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": "invalid", "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert "INVALID_FORMAT" in call_args[0][2]["code"] @pytest.mark.asyncio async def test_defensive_replacement_game_not_found(self, mock_manager): """Verify error when game doesn't exist.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = None from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(uuid4()), "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) assert "not found" in mock_manager.emit_to_user.call_args[0][2]["message"] @pytest.mark.asyncio async def test_defensive_replacement_missing_position( self, mock_manager, mock_game_state ): """Verify error when new_position is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "position" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_defensive_replacement_validation_failure( self, mock_manager, mock_game_state, mock_sub_result_failure ): """Verify SubstitutionManager failure is propagated.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def mock_lock(game_id, timeout=30.0): yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = mock_lock mock_sub_instance = MagicMock() mock_sub_instance.defensive_replace = AsyncMock( return_value=mock_sub_result_failure ) MockSubMgr.return_value = mock_sub_instance from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" @pytest.mark.asyncio async def test_defensive_replacement_lock_timeout( self, mock_manager, mock_game_state ): """Verify lock timeout returns server busy message.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def timeout_lock(game_id, timeout=30.0): raise asyncio.TimeoutError("Lock timeout") yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = timeout_lock from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert "busy" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_defensive_replacement_missing_player_out( self, mock_manager, mock_game_state ): """ Verify error when player_out_lineup_id is missing. Defensive replacement requires specifying which player is being replaced. """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_in_card_id": 999, "new_position": "CF", "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "player_out" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_defensive_replacement_missing_player_in( self, mock_manager, mock_game_state ): """ Verify error when player_in_card_id is missing. Defensive replacement requires specifying which player is entering. """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "new_position": "CF", "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "player_in" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_defensive_replacement_missing_team_id( self, mock_manager, mock_game_state ): """ Verify error when team_id is missing. Defensive replacement must specify which team is making the substitution. """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_defensive_replacement"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "new_position": "CF", }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" assert "team_id" in call_args[0][2]["message"].lower() # ============================================================================ # PITCHING CHANGE TESTS # ============================================================================ class TestPitchingChange: """Tests for request_pitching_change handler.""" @pytest.mark.asyncio async def test_pitching_change_success( self, mock_manager, mock_game_state, mock_sub_result_success ): """Verify successful pitching change.""" from socketio import AsyncServer sio = AsyncServer() mock_sub_result_success.new_position = "P" # Override for pitcher @asynccontextmanager async def mock_lock(game_id, timeout=30.0): yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = mock_lock mock_sub_instance = MagicMock() mock_sub_instance.change_pitcher = AsyncMock( return_value=mock_sub_result_success ) MockSubMgr.return_value = mock_sub_instance from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) mock_sub_instance.change_pitcher.assert_called_once() broadcast_call = mock_manager.broadcast_to_game.call_args assert broadcast_call[0][2]["type"] == "pitching_change" @pytest.mark.asyncio async def test_pitching_change_missing_game_id(self, mock_manager): """Verify error when game_id is missing.""" from socketio import AsyncServer from app.websocket.handlers import register_handlers sio = AsyncServer() register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", {"player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1}, ) call_args = mock_manager.emit_to_user.call_args assert "MISSING_FIELD" in call_args[0][2]["code"] @pytest.mark.asyncio async def test_pitching_change_invalid_game_id(self, mock_manager): """Verify error when game_id is invalid.""" from socketio import AsyncServer from app.websocket.handlers import register_handlers sio = AsyncServer() register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": "bad-uuid", "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert "INVALID_FORMAT" in call_args[0][2]["code"] @pytest.mark.asyncio async def test_pitching_change_game_not_found(self, mock_manager): """Verify error when game doesn't exist.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = None from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(uuid4()), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) assert "not found" in mock_manager.emit_to_user.call_args[0][2]["message"] @pytest.mark.asyncio async def test_pitching_change_missing_player_out( self, mock_manager, mock_game_state ): """Verify error when player_out_lineup_id is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" @pytest.mark.asyncio async def test_pitching_change_missing_player_in( self, mock_manager, mock_game_state ): """Verify error when player_in_card_id is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" @pytest.mark.asyncio async def test_pitching_change_missing_team_id(self, mock_manager, mock_game_state): """Verify error when team_id is missing.""" from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" @pytest.mark.asyncio async def test_pitching_change_validation_failure( self, mock_manager, mock_game_state, mock_sub_result_failure ): """Verify SubstitutionManager failure is propagated.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def mock_lock(game_id, timeout=30.0): yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = mock_lock mock_sub_instance = MagicMock() mock_sub_instance.change_pitcher = AsyncMock( return_value=mock_sub_result_failure ) MockSubMgr.return_value = mock_sub_instance from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "substitution_error" @pytest.mark.asyncio async def test_pitching_change_lock_timeout(self, mock_manager, mock_game_state): """Verify lock timeout returns server busy message.""" from socketio import AsyncServer sio = AsyncServer() @asynccontextmanager async def timeout_lock(game_id, timeout=30.0): raise asyncio.TimeoutError("Lock timeout") yield with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_state.return_value = mock_game_state mock_state_mgr.game_lock = timeout_lock from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["request_pitching_change"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1, }, ) call_args = mock_manager.emit_to_user.call_args assert "busy" in call_args[0][2]["message"].lower()