Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

931 lines
31 KiB
Python

"""
Tests for the SocketIOService class.
Tests cover:
- Singleton pattern
- Event emission
- Subscriber management
- Error handling
"""
from unittest.mock import MagicMock, patch
class MockFlaskApp:
"""Mock Flask application for testing."""
def __init__(self):
self.config = {}
self.debug = False
class MockSocketIO:
"""Mock SocketIO for testing."""
def __init__(self, app=None, **kwargs):
self.app = app
self.kwargs = kwargs
self.emitted_events = []
self._handlers = {}
def emit(self, event, data, room=None):
self.emitted_events.append({"event": event, "data": data, "room": room})
def on(self, event):
def decorator(f):
self._handlers[event] = f
return f
return decorator
@property
def on_error(self):
def decorator(f):
self._handlers["error"] = f
return f
return decorator
@property
def on_error_default(self):
def decorator(f):
self._handlers["error_default"] = f
return f
return decorator
def run(self, app, **kwargs):
pass
class TestSocketIOServiceSingleton:
"""Tests for SocketIOService singleton pattern."""
def test_singleton_requires_app_first_time(self):
"""SocketIOService requires Flask app on first instantiation."""
# Reset singleton for test
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
# Store and reset singleton
original_instance = SocketIOService._instance
SocketIOService._instance = None
try:
# Should raise ValueError when no app provided
try:
SocketIOService()
assert False, "Should have raised ValueError"
except ValueError as e:
assert "Flask app must be specified" in str(e)
finally:
# Restore original singleton
SocketIOService._instance = original_instance
class TestSocketIOServiceEmit:
"""Tests for SocketIOService emit methods."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_socket_event_broadcast(self, mock_socketio_class):
"""emit_socket_event broadcasts to all clients when no room specified."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
# Reset and create fresh singleton
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
result = service.emit_socket_event("test_event", {"data": "value"})
assert result is True
mock_socketio.emit.assert_called_with(
"test_event", {"data": "value"}
)
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_socket_event_to_room(self, mock_socketio_class):
"""emit_socket_event sends to specific room when specified."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
result = service.emit_socket_event(
"test_event", {"data": "value"}, room="room123"
)
assert result is True
mock_socketio.emit.assert_called_with(
"test_event", {"data": "value"}, room="room123"
)
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_socket_event_handles_error(self, mock_socketio_class):
"""emit_socket_event returns False on error."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio.emit.side_effect = Exception("Connection error")
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
result = service.emit_socket_event("test_event", {"data": "value"})
assert result is False
finally:
SocketIOService._instance = original_instance
class TestSocketIOServiceSubscribers:
"""Tests for SocketIOService subscriber management."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_to_subscribers_success(self, mock_socketio_class):
"""emit_to_subscribers emits per-sid to the research channel.
emit only fires when at least one subscriber sid is registered; we
pre-populate one and verify the per-sid room-targeted emit.
"""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Pre-register a subscriber sid so emit has somewhere to go.
with service._SocketIOService__lock:
service._SocketIOService__socket_subscriptions[
"research_123"
] = {"sid-1"}
result = service.emit_to_subscribers(
"progress", "research_123", {"percent": 50}
)
assert result is True
# Should emit to the formatted channel, scoped to the sid.
mock_socketio.emit.assert_called_with(
"progress_research_123", {"percent": 50}, room="sid-1"
)
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_to_subscribers_swallows_inner_error(
self, mock_socketio_class
):
"""A per-subscriber emit failure is logged but does not fail the
whole call — the outer ``return True`` path still wins so other
subscribers in the same set can still receive the event.
"""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio.emit.side_effect = Exception("Emit failed")
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
with service._SocketIOService__lock:
service._SocketIOService__socket_subscriptions[
"research_123"
] = {"sid-1"}
result = service.emit_to_subscribers(
"progress", "research_123", {"percent": 50}
)
# Per-sid failures are caught and logged; outer call still
# succeeds so the loop can keep delivering to other sids.
assert result is True
mock_socketio.emit.assert_called_with(
"progress_research_123", {"percent": 50}, room="sid-1"
)
finally:
SocketIOService._instance = original_instance
class TestSocketIOServiceRun:
"""Tests for SocketIOService run method."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_run_starts_server(self, mock_socketio_class):
"""run method starts the SocketIO server."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Mock run to not block
mock_socketio.run = MagicMock()
service.run(host="0.0.0.0", port=5000, debug=False)
mock_socketio.run.assert_called_once()
call_kwargs = mock_socketio.run.call_args
assert call_kwargs[1]["host"] == "0.0.0.0"
assert call_kwargs[1]["port"] == 5000
assert call_kwargs[1]["debug"] is False
finally:
SocketIOService._instance = original_instance
class TestSocketIOServiceInit:
"""Tests for SocketIOService initialization."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_service_initializes_with_app(self, mock_socketio_class):
"""Service initializes properly with Flask app."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Service should be initialized
assert service is not None
# SocketIO should have been created
mock_socketio_class.assert_called_once()
finally:
SocketIOService._instance = original_instance
class TestSocketIOServiceMultipleEmits:
"""Tests for multiple emit scenarios."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_multiple_events_same_room(self, mock_socketio_class):
"""Multiple events can be emitted to the same room."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
service.emit_socket_event("event1", {"data": 1}, room="room1")
service.emit_socket_event("event2", {"data": 2}, room="room1")
assert mock_socketio.emit.call_count == 2
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_with_namespace(self, mock_socketio_class):
"""Events can be emitted to specific namespaces."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Test emit with namespace if supported
result = service.emit_socket_event(
"test_event",
{"data": "value"},
)
assert result is True
finally:
SocketIOService._instance = original_instance
class TestHandleSubscribe:
"""Tests for subscribe event handling."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_subscribe_adds_to_room(self, mock_socketio_class):
"""Subscribe event should add client to research room."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Service should register subscribe handler
assert service is not None
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_subscribe_with_research_id(self, mock_socketio_class):
"""Subscribe with valid research_id."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Service should be ready to handle subscriptions
# The service initializes internal handlers
assert service is not None
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_subscribe_without_research_id_is_handled(
self, mock_socketio_class
):
"""Subscribe without research_id should be handled gracefully."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Service should be able to handle missing research_id
assert service is not None
finally:
SocketIOService._instance = original_instance
class TestEmitToSubscribersAdvanced:
"""Advanced tests for emit_to_subscribers method."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_progress_update(self, mock_socketio_class):
"""Emit progress update to a registered subscriber.
emit only fires per-sid, so we register a fake subscriber before
calling emit_to_subscribers.
"""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
with service._SocketIOService__lock:
service._SocketIOService__socket_subscriptions[
"research_abc123"
] = {"sid-x"}
result = service.emit_to_subscribers(
"progress",
"research_abc123",
{"current": 50, "total": 100, "message": "Processing..."},
)
assert result is True
mock_socketio.emit.assert_called_once()
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_status_update(self, mock_socketio_class):
"""Emit status update to subscribers."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
result = service.emit_to_subscribers(
"status",
"research_xyz789",
{"status": "completed", "result_url": "/results/xyz789"},
)
assert result is True
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_emit_error_to_subscribers(self, mock_socketio_class):
"""Emit error to subscribers."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
result = service.emit_to_subscribers(
"error",
"research_failed123",
{"error": "Research failed", "code": 500},
)
assert result is True
finally:
SocketIOService._instance = original_instance
class TestSocketIOServiceSingletonBehavior:
"""Tests for SocketIOService singleton behavior."""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_singleton_returns_same_instance(self, mock_socketio_class):
"""Second call returns same instance."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service1 = SocketIOService(app=app)
service2 = SocketIOService()
assert service1 is service2
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_singleton_ignores_second_app(self, mock_socketio_class):
"""Second app parameter is ignored."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app1 = MockFlaskApp()
app2 = MockFlaskApp()
service1 = SocketIOService(app=app1)
service2 = SocketIOService(app=app2)
# Should be same instance, app2 ignored
assert service1 is service2
finally:
SocketIOService._instance = original_instance
class TestSocketServiceDisconnectCleanup:
"""Tests for thread cleanup on socket disconnect.
These tests verify that __handle_disconnect properly calls
cleanup_current_thread() to prevent file descriptor leaks from
unclosed SQLAlchemy sessions in socket handler threads.
"""
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_handler_exists(self, mock_socketio_class):
"""Test that the disconnect handler is registered."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# The service should register handlers via @socketio.on decorators
# We verify the service initializes without error
assert service is not None
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_removes_subscriptions(self, mock_socketio_class):
"""Test that disconnect 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.
"""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Access the private subscriptions dict
subscriptions = service._SocketIOService__socket_subscriptions
# Set up correct schema: research_id → {sids}
test_sid = "test_client_123"
subscriptions["research_1"] = {test_sid, "other_client"}
subscriptions["research_2"] = {test_sid}
# Create a mock request
mock_request = MagicMock()
mock_request.sid = test_sid
# Call the disconnect handler directly
service._SocketIOService__handle_disconnect(
mock_request, "test reason"
)
# sid should be removed from research_1, leaving other_client
assert test_sid not in subscriptions.get("research_1", set())
assert "other_client" in subscriptions["research_1"]
# research_2 had only this sid, so the key should be cleaned up
assert "research_2" not in subscriptions
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_leaves_other_clients_intact(self, mock_socketio_class):
"""Test that disconnect only removes the disconnecting client."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
subscriptions = service._SocketIOService__socket_subscriptions
subscriptions["research_1"] = {"client_A", "client_B", "client_C"}
mock_request = MagicMock()
mock_request.sid = "client_B"
service._SocketIOService__handle_disconnect(
mock_request, "test reason"
)
assert subscriptions["research_1"] == {"client_A", "client_C"}
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_noop_when_sid_not_subscribed(self, mock_socketio_class):
"""Test that disconnect is a no-op when sid has no subscriptions."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
subscriptions = service._SocketIOService__socket_subscriptions
subscriptions["research_1"] = {"other_client_1", "other_client_2"}
mock_request = MagicMock()
mock_request.sid = "unknown_client"
service._SocketIOService__handle_disconnect(
mock_request, "test reason"
)
# Nothing should change
assert subscriptions["research_1"] == {
"other_client_1",
"other_client_2",
}
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_subscribe_then_disconnect_round_trip(self, mock_socketio_class):
"""Test full subscribe → disconnect cycle uses consistent schema."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
mock_request = MagicMock()
mock_request.sid = "round_trip_client"
# Subscribe to two research IDs
service._SocketIOService__handle_subscribe(
{"research_id": "r1"}, mock_request
)
service._SocketIOService__handle_subscribe(
{"research_id": "r2"}, mock_request
)
subscriptions = service._SocketIOService__socket_subscriptions
assert "round_trip_client" in subscriptions["r1"]
assert "round_trip_client" in subscriptions["r2"]
# Disconnect should clean up both
service._SocketIOService__handle_disconnect(
mock_request, "test reason"
)
assert "round_trip_client" not in subscriptions.get("r1", set())
assert "round_trip_client" not in subscriptions.get("r2", set())
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_calls_cleanup_current_thread(self, mock_socketio_class):
"""Test that disconnect handler calls cleanup_current_thread()."""
import inspect
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
# Verify by source code inspection that cleanup_current_thread
# is called in the disconnect handler
func_source = inspect.getsource(
service._SocketIOService__handle_disconnect
)
# Check that the cleanup function is imported and called
assert "cleanup_current_thread" in func_source, (
"Disconnect handler should call cleanup_current_thread()"
)
assert (
"from ...database.thread_local_session import" in func_source
), "Disconnect handler should import from thread_local_session"
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_handles_cleanup_import_error_gracefully(
self, mock_socketio_class
):
"""Test that disconnect handles ImportError gracefully."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
mock_request = MagicMock()
mock_request.sid = "test_client_import_error"
# The handler should handle ImportError gracefully (pass)
# This is already handled in the source code with:
# except ImportError: pass
# Call should not raise even if import fails
# (the actual import might succeed or fail depending on environment)
try:
service._SocketIOService__handle_disconnect(
mock_request, "import error test"
)
except ImportError:
# Should not propagate
assert False, "ImportError should be caught internally"
# Handler completed without propagating ImportError
assert True
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_handles_cleanup_exception_gracefully(
self, mock_socketio_class
):
"""Test that disconnect handles cleanup exceptions gracefully."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
mock_request = MagicMock()
mock_request.sid = "test_client_cleanup_error"
# The handler has a try/except block that catches Exception
# and logs it rather than propagating
# Call should not raise
try:
service._SocketIOService__handle_disconnect(
mock_request, "cleanup error test"
)
except Exception as e:
# Only outer exceptions should propagate, not cleanup errors
# The source has: except Exception: self.__log_exception(...)
if "Error cleaning up thread session" in str(e):
assert False, (
"Cleanup exception should be caught internally"
)
# Handler completed
assert True
finally:
SocketIOService._instance = original_instance
@patch("local_deep_research.web.services.socket_service.SocketIO")
def test_disconnect_logs_client_info(self, mock_socketio_class):
"""Test that disconnect logs client disconnect information."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
original_instance = SocketIOService._instance
SocketIOService._instance = None
mock_socketio = MagicMock()
mock_socketio_class.return_value = mock_socketio
try:
app = MockFlaskApp()
service = SocketIOService(app=app)
mock_request = MagicMock()
mock_request.sid = "logged_client_123"
# Mock the logging method
with patch.object(
service, "_SocketIOService__log_info"
) as mock_log_info:
service._SocketIOService__handle_disconnect(
mock_request, "client initiated"
)
# Should log disconnect info
assert mock_log_info.called
# Check that the client ID appears in at least one log call
log_messages = [
str(call) for call in mock_log_info.call_args_list
]
assert any(
"logged_client_123" in msg for msg in log_messages
), "Client ID should be logged on disconnect"
finally:
SocketIOService._instance = original_instance