""" Comprehensive tests for SocketIOService. Tests singleton pattern, event emission, subscriptions, and error handling. """ import pytest from unittest.mock import Mock, MagicMock, patch class TestSocketIOServiceSingleton: """Tests for SocketIOService singleton pattern. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ def test_requires_app_on_first_creation(self): """Test that Flask app is required on first creation.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) with pytest.raises(ValueError) as exc_info: SocketIOService() assert "Flask app must be specified" in str(exc_info.value) def test_creates_instance_with_app(self, mock_flask_app): """Test that instance is created with Flask app.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) with patch( "local_deep_research.web.services.socket_service.SocketIO" ) as mock_socketio: service = SocketIOService(app=mock_flask_app) assert service is not None mock_socketio.assert_called_once() def test_returns_same_instance_on_second_call(self, mock_flask_app): """Test that same instance is returned on subsequent calls.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) with patch("local_deep_research.web.services.socket_service.SocketIO"): service1 = SocketIOService(app=mock_flask_app) service2 = SocketIOService() assert service1 is service2 def test_ignores_app_on_second_call(self, mock_flask_app): """Test that app parameter is ignored after singleton creation.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) with patch("local_deep_research.web.services.socket_service.SocketIO"): service1 = SocketIOService(app=mock_flask_app) other_app = Mock() service2 = SocketIOService(app=other_app) assert service1 is service2 class TestSocketIOServiceInit: """Tests for SocketIOService initialization. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ def test_initializes_socketio_with_correct_params(self, mock_flask_app): """Test that SocketIO is initialized with correct parameters.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) with patch( "local_deep_research.web.services.socket_service.SocketIO" ) as mock_socketio: SocketIOService(app=mock_flask_app) mock_socketio.assert_called_once_with( mock_flask_app, cors_allowed_origins=None, async_mode="threading", path="/socket.io", logger=False, engineio_logger=False, ping_timeout=20, ping_interval=5, ) def test_registers_connect_handler(self, mock_flask_app): """Test that connect handler is registered.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): SocketIOService(app=mock_flask_app) # Check that on("connect") was called mock_socketio.on.assert_any_call("connect") def test_registers_disconnect_handler(self, mock_flask_app): """Test that disconnect handler is registered.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): SocketIOService(app=mock_flask_app) mock_socketio.on.assert_any_call("disconnect") def test_registers_subscribe_handler(self, mock_flask_app): """Test that subscribe_to_research handler is registered.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): SocketIOService(app=mock_flask_app) mock_socketio.on.assert_any_call("subscribe_to_research") def test_registers_join_alias_handler(self, mock_flask_app): """Test that the 'join' alias handler is registered. The JS client emits 'join' on subscribe; without this alias, the catch-up snapshot in __handle_subscribe never fires for fresh page loads and per-client targeting falls through to broadcast. """ from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): SocketIOService(app=mock_flask_app) mock_socketio.on.assert_any_call("join") def test_registers_leave_alias_handler(self, mock_flask_app): """Test that the 'leave' alias handler is registered.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): SocketIOService(app=mock_flask_app) mock_socketio.on.assert_any_call("leave") def test_registers_unsubscribe_handler(self, mock_flask_app): """Test that unsubscribe_from_research handler is registered.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): SocketIOService(app=mock_flask_app) mock_socketio.on.assert_any_call("unsubscribe_from_research") class TestSocketIOServiceEmitSocketEvent: """Tests for emit_socket_event method. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ @pytest.fixture def service(self, mock_flask_app): """Create SocketIOService instance with mocked SocketIO.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): service = SocketIOService(app=mock_flask_app) service._SocketIOService__socketio = mock_socketio return service, mock_socketio def test_emits_to_all_when_no_room(self, service): """Test emitting to all clients when no room specified.""" svc, mock_socketio = service result = svc.emit_socket_event("test_event", {"key": "value"}) assert result is True mock_socketio.emit.assert_called_once_with( "test_event", {"key": "value"} ) def test_emits_to_specific_room(self, service): """Test emitting to specific room.""" svc, mock_socketio = service result = svc.emit_socket_event( "test_event", {"key": "value"}, room="room-123" ) assert result is True mock_socketio.emit.assert_called_once_with( "test_event", {"key": "value"}, room="room-123" ) def test_returns_false_on_exception(self, service): """Test that False is returned on emission error.""" svc, mock_socketio = service mock_socketio.emit.side_effect = Exception("Emission failed") result = svc.emit_socket_event("test_event", {"key": "value"}) assert result is False def test_logs_exception_on_error(self, service): """Test that exception is logged on error.""" svc, mock_socketio = service mock_socketio.emit.side_effect = Exception("Emission failed") with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: svc.emit_socket_event("test_event", {"key": "value"}) mock_logger.exception.assert_called_once() class TestSocketIOServiceEmitToSubscribers: """Tests for emit_to_subscribers method. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ @pytest.fixture def service(self, mock_flask_app): """Create SocketIOService instance with mocked SocketIO.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): service = SocketIOService(app=mock_flask_app) service._SocketIOService__socketio = mock_socketio return service, mock_socketio def test_emits_formatted_event(self, service, sample_research_id): """Test that event is emitted with formatted name to subscribers.""" svc, mock_socketio = service # Targeted delivery is the only path emit_to_subscribers takes; # without a registered subscriber the event is dropped (the # no-subscribers test pins that contract separately). svc._SocketIOService__socket_subscriptions = { sample_research_id: {"sid-1"} } svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"} ) # Should emit with formatted event name, scoped to the room. mock_socketio.emit.assert_any_call( f"research_update_{sample_research_id}", {"data": "test"}, room="sid-1", ) def test_emits_to_individual_subscribers(self, service, sample_research_id): """Test that event is emitted to each subscriber.""" svc, mock_socketio = service # Add subscribers svc._SocketIOService__socket_subscriptions = { sample_research_id: {"sid-1", "sid-2"} } svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"} ) # Should emit to each subscriber calls = mock_socketio.emit.call_args_list room_calls = [c for c in calls if "room" in c.kwargs] assert len(room_calls) == 2 def test_returns_true_on_success(self, service, sample_research_id): """Test that True is returned on success.""" svc, mock_socketio = service result = svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"} ) assert result is True def test_returns_false_on_exception(self, service, sample_research_id): """Test that False is returned on error. Per-subscriber emit failures are now caught inside the inner loop (so other subscribers still receive the event), so the method returns False only when something outside the loop raises — simulated here by raising during ``__lock`` acquisition. """ svc, mock_socketio = service # Make the lock context manager raise to force the outer # except path. The lock is a private attribute on the service. broken_lock = MagicMock() broken_lock.__enter__ = MagicMock(side_effect=Exception("Failed")) broken_lock.__exit__ = MagicMock(return_value=False) svc._SocketIOService__lock = broken_lock result = svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"} ) assert result is False def test_disables_logging_when_requested(self, service, sample_research_id): """Test that logging can be disabled.""" svc, mock_socketio = service svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"}, enable_logging=False, ) # After call, logging should be re-enabled assert svc._SocketIOService__logging_enabled is True def test_re_enables_logging_after_exception( self, service, sample_research_id ): """Test that logging is re-enabled even after exception.""" svc, mock_socketio = service mock_socketio.emit.side_effect = Exception("Failed") svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"}, enable_logging=False, ) # Logging should be re-enabled after exception assert svc._SocketIOService__logging_enabled is True def test_handles_no_subscribers(self, service, sample_research_id): """No-subscribers: drop the event rather than broadcasting. A prior implementation broadcast room-less to all clients in this case, which leaked research payloads across users. Early events are recovered by the catch-up snapshot in ``__handle_subscribe``; no cross-user broadcast is needed. """ svc, mock_socketio = service result = svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"} ) assert result is True # MUST NOT emit when there are no targeted subscribers — the # absence of a room= argument would broadcast to every client. mock_socketio.emit.assert_not_called() def test_handles_subscriber_emit_error(self, service, sample_research_id): """Test that individual subscriber errors don't stop other emissions.""" svc, mock_socketio = service # Add subscribers svc._SocketIOService__socket_subscriptions = { sample_research_id: {"sid-1", "sid-2"} } # First call succeeds, second fails call_count = [0] def emit_side_effect(*args, **kwargs): call_count[0] += 1 if call_count[0] == 2: # Fail on second subscriber raise Exception("Subscriber error") mock_socketio.emit.side_effect = emit_side_effect result = svc.emit_to_subscribers( "research_update", sample_research_id, {"data": "test"} ) # Should still return True (overall success) assert result is True class TestSocketIOServiceSubscriptionManagement: """Tests for subscription management. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ @pytest.fixture def service_with_mocks(self, mock_flask_app): """Create service with accessible internal state.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): service = SocketIOService(app=mock_flask_app) service._SocketIOService__socketio = mock_socketio return service, mock_socketio def test_subscribe_adds_client( self, service_with_mocks, mock_request, sample_research_id ): """Test that subscribing adds client to subscription set.""" svc, _ = service_with_mocks data = {"research_id": sample_research_id} svc._SocketIOService__handle_subscribe(data, mock_request) subscriptions = svc._SocketIOService__socket_subscriptions assert sample_research_id in subscriptions assert mock_request.sid in subscriptions[sample_research_id] def test_subscribe_creates_set_for_new_research( self, service_with_mocks, mock_request ): """Test that subscribing creates new set for new research.""" svc, _ = service_with_mocks data = {"research_id": "new-research-id"} svc._SocketIOService__handle_subscribe(data, mock_request) subscriptions = svc._SocketIOService__socket_subscriptions assert "new-research-id" in subscriptions assert isinstance(subscriptions["new-research-id"], set) def test_subscribe_ignores_empty_research_id( self, service_with_mocks, mock_request ): """Test that empty research_id is ignored.""" svc, _ = service_with_mocks data = {"research_id": ""} svc._SocketIOService__handle_subscribe(data, mock_request) subscriptions = svc._SocketIOService__socket_subscriptions assert "" not in subscriptions def test_subscribe_ignores_missing_research_id( self, service_with_mocks, mock_request ): """Test that missing research_id is ignored.""" svc, _ = service_with_mocks data = {} svc._SocketIOService__handle_subscribe(data, mock_request) subscriptions = svc._SocketIOService__socket_subscriptions assert len(subscriptions) == 0 def test_disconnect_removes_subscription( self, service_with_mocks, mock_request ): """Test that disconnecting removes client from all research subscriptions. Schema: __socket_subscriptions is research_id → set of sids. Disconnect must discard the sid from every research_id's set. """ svc, _ = service_with_mocks # Set up correct schema: research_id → {sids} svc._SocketIOService__socket_subscriptions = { "research-1": {mock_request.sid, "other-sid"}, "research-2": {mock_request.sid}, } svc._SocketIOService__handle_disconnect( mock_request, "client disconnect" ) subscriptions = svc._SocketIOService__socket_subscriptions # sid should be removed from research-1, leaving only other-sid assert mock_request.sid not in subscriptions.get("research-1", set()) assert "other-sid" in subscriptions["research-1"] # research-2 had only this sid, so the key should be cleaned up assert "research-2" not in subscriptions def test_disconnect_handles_no_subscription( self, service_with_mocks, mock_request ): """Test that disconnect handles non-subscribed client.""" svc, _ = service_with_mocks # No subscriptions exist svc._SocketIOService__socket_subscriptions = {} # Should not raise svc._SocketIOService__handle_disconnect( mock_request, "client disconnect" ) def test_disconnect_leaves_other_clients_intact( self, service_with_mocks, mock_request ): """Test that disconnect only removes the disconnecting client.""" svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = { "research-1": {mock_request.sid, "sid-A", "sid-B"}, } svc._SocketIOService__handle_disconnect( mock_request, "client disconnect" ) subscriptions = svc._SocketIOService__socket_subscriptions assert subscriptions["research-1"] == {"sid-A", "sid-B"} def test_disconnect_noop_when_sid_not_subscribed( self, service_with_mocks, mock_request ): """Test that disconnect is a no-op when sid has no subscriptions.""" svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = { "research-1": {"other-sid-1", "other-sid-2"}, } svc._SocketIOService__handle_disconnect( mock_request, "client disconnect" ) # Nothing should change subscriptions = svc._SocketIOService__socket_subscriptions assert subscriptions["research-1"] == {"other-sid-1", "other-sid-2"} def test_subscribe_then_disconnect_round_trip( self, service_with_mocks, mock_request ): """Test full subscribe → disconnect cycle uses consistent schema.""" svc, _ = service_with_mocks # Subscribe to two research IDs svc._SocketIOService__handle_subscribe( {"research_id": "r1"}, mock_request ) svc._SocketIOService__handle_subscribe( {"research_id": "r2"}, mock_request ) subscriptions = svc._SocketIOService__socket_subscriptions assert mock_request.sid in subscriptions["r1"] assert mock_request.sid in subscriptions["r2"] # Disconnect should clean up both svc._SocketIOService__handle_disconnect( mock_request, "client disconnect" ) assert mock_request.sid not in subscriptions.get("r1", set()) assert mock_request.sid not in subscriptions.get("r2", set()) def test_unsubscribe_discards_sid( self, service_with_mocks, mock_request, sample_research_id ): """Unsubscribe handler removes the sid from the subscription set.""" svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = { sample_research_id: {mock_request.sid, "other-sid"}, } svc._SocketIOService__handle_unsubscribe( {"research_id": sample_research_id}, mock_request ) subscriptions = svc._SocketIOService__socket_subscriptions assert mock_request.sid not in subscriptions[sample_research_id] # Other clients are untouched assert "other-sid" in subscriptions[sample_research_id] def test_unsubscribe_ignores_missing_research_id( self, service_with_mocks, mock_request ): """Unsubscribe with missing research_id is a no-op.""" svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = { "research-1": {"sid-A"}, } # Should not raise svc._SocketIOService__handle_unsubscribe({}, mock_request) svc._SocketIOService__handle_unsubscribe(None, mock_request) # Existing subscriptions are unchanged subscriptions = svc._SocketIOService__socket_subscriptions assert subscriptions["research-1"] == {"sid-A"} def test_unsubscribe_handles_unknown_research_id( self, service_with_mocks, mock_request ): """Unsubscribe for a research_id with no subscribers is a no-op.""" svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = {} # Should not raise svc._SocketIOService__handle_unsubscribe( {"research_id": "never-subscribed"}, mock_request ) def test_unsubscribe_prunes_empty_subscription_set( self, service_with_mocks, mock_request, sample_research_id ): """Removing the last sid for a research_id deletes the dict entry. Otherwise __socket_subscriptions accumulates stale keys for every research_id seen over a long-running server, even after all clients have left. """ svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = { sample_research_id: {mock_request.sid}, } svc._SocketIOService__handle_unsubscribe( {"research_id": sample_research_id}, mock_request ) assert ( sample_research_id not in svc._SocketIOService__socket_subscriptions ) def test_unsubscribe_keeps_set_when_other_clients_remain( self, service_with_mocks, mock_request, sample_research_id ): """Unsubscribe must not delete the set while other sids are present.""" svc, _ = service_with_mocks svc._SocketIOService__socket_subscriptions = { sample_research_id: {mock_request.sid, "other-sid"}, } svc._SocketIOService__handle_unsubscribe( {"research_id": sample_research_id}, mock_request ) assert svc._SocketIOService__socket_subscriptions[ sample_research_id ] == {"other-sid"} class TestSocketIOServiceErrorHandling: """Tests for error handling methods. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ @pytest.fixture def service(self, mock_flask_app): """Create SocketIOService instance.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): service = SocketIOService(app=mock_flask_app) return service def test_socket_error_returns_false(self, service): """Test that socket error handler returns False.""" result = service._SocketIOService__handle_socket_error( Exception("Test error") ) assert result is False def test_default_error_returns_false(self, service): """Test that default error handler returns False.""" result = service._SocketIOService__handle_default_error( Exception("Test error") ) assert result is False def test_connect_handler_rejects_unauthenticated( self, service, mock_request ): """Connect handler rejects when no session username.""" with patch( "local_deep_research.web.services.socket_service.session", {} ): assert ( service._SocketIOService__handle_connect(mock_request) is False ) def test_connect_handler_rejects_no_db_session_and_no_password( self, service, mock_request ): """Connect handler rejects when user has no active DB session and no stored password.""" with ( patch( "local_deep_research.web.services.socket_service.session", {"username": "alice", "session_id": "sess-1"}, ), patch( "local_deep_research.web.services.socket_service.db_manager" ) as mock_db, patch( "local_deep_research.web.services.socket_service.session_password_store" ) as mock_store, ): mock_db.is_user_connected.return_value = False mock_store.get_session_password.return_value = None assert ( service._SocketIOService__handle_connect(mock_request) is False ) mock_db.open_user_database.assert_not_called() def test_connect_handler_lazy_opens_when_password_available( self, service, mock_request ): """Connect handler lazy-opens the DB when the engine isn't open but a password is stored.""" with ( patch( "local_deep_research.web.services.socket_service.session", {"username": "alice", "session_id": "sess-1"}, ), patch( "local_deep_research.web.services.socket_service.db_manager" ) as mock_db, patch( "local_deep_research.web.services.socket_service.session_password_store" ) as mock_store, patch("local_deep_research.web.services.socket_service.join_room"), ): mock_db.is_user_connected.return_value = False mock_store.get_session_password.return_value = "pw" assert ( service._SocketIOService__handle_connect(mock_request) is True ) mock_db.open_user_database.assert_called_once_with("alice", "pw") def test_connect_handler_accepts_authenticated(self, service, mock_request): """Connect handler accepts authenticated users with active DB session.""" with ( patch( "local_deep_research.web.services.socket_service.session", {"username": "alice"}, ), patch( "local_deep_research.web.services.socket_service.db_manager" ) as mock_db, patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger, patch("local_deep_research.web.services.socket_service.join_room"), ): mock_db.is_user_connected.return_value = True assert ( service._SocketIOService__handle_connect(mock_request) is True ) mock_logger.info.assert_called() call_args = mock_logger.info.call_args[0][0] assert mock_request.sid in call_args class TestSocketIOServiceRun: """Tests for run method. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ @pytest.fixture def service(self, mock_flask_app): """Create SocketIOService instance.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): service = SocketIOService(app=mock_flask_app) service._SocketIOService__socketio = mock_socketio return service, mock_socketio, mock_flask_app def test_run_calls_socketio_run(self, service): """Test that run method calls socketio.run.""" svc, mock_socketio, mock_app = service svc.run("0.0.0.0", 5000) mock_socketio.run.assert_called_once() def test_run_passes_correct_params(self, service): """Test that run passes correct parameters.""" svc, mock_socketio, mock_app = service svc.run("localhost", 8080, debug=True) mock_socketio.run.assert_called_once_with( mock_app, debug=True, host="localhost", port=8080, allow_unsafe_werkzeug=True, use_reloader=False, ) def test_run_defaults_debug_to_false(self, service): """Test that debug defaults to False.""" svc, mock_socketio, mock_app = service svc.run("0.0.0.0", 5000) call_kwargs = mock_socketio.run.call_args[1] assert call_kwargs["debug"] is False class TestSocketIOServiceLogging: """Tests for logging behavior. Note: Singleton reset is handled by the global reset_all_singletons fixture in tests/conftest.py. """ @pytest.fixture def service(self, mock_flask_app): """Create SocketIOService instance.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) mock_socketio = MagicMock() with patch( "local_deep_research.web.services.socket_service.SocketIO", return_value=mock_socketio, ): service = SocketIOService(app=mock_flask_app) return service def test_logging_enabled_by_default(self, service): """Test that logging is enabled by default.""" assert service._SocketIOService__logging_enabled is True def test_log_info_logs_when_enabled(self, service): """Test that __log_info logs when enabled.""" with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: service._SocketIOService__log_info("Test message") mock_logger.info.assert_called_once_with("Test message") def test_log_info_skips_when_disabled(self, service): """Test that __log_info skips when disabled.""" service._SocketIOService__logging_enabled = False with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: service._SocketIOService__log_info("Test message") mock_logger.info.assert_not_called() def test_log_error_logs_when_enabled(self, service): """Test that __log_error logs when enabled.""" with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: service._SocketIOService__log_error("Error message") mock_logger.error.assert_called_once_with("Error message") def test_log_error_skips_when_disabled(self, service): """Test that __log_error skips when disabled.""" service._SocketIOService__logging_enabled = False with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: service._SocketIOService__log_error("Error message") mock_logger.error.assert_not_called() def test_log_exception_logs_when_enabled(self, service): """Test that __log_exception logs when enabled.""" with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: service._SocketIOService__log_exception("Exception message") mock_logger.exception.assert_called_once_with("Exception message") def test_log_exception_skips_when_disabled(self, service): """Test that __log_exception skips when disabled.""" service._SocketIOService__logging_enabled = False with patch( "local_deep_research.web.services.socket_service.logger" ) as mock_logger: service._SocketIOService__log_exception("Exception message") mock_logger.exception.assert_not_called()