""" Tests for the QueueProcessorV2 class. Tests cover: - Initialization - Starting and stopping - User activity notification - Research queuing - Completion notifications - Error handling """ import threading from unittest.mock import Mock, MagicMock, patch class TestQueueProcessorV2Init: """Tests for QueueProcessorV2 initialization.""" def test_init_default_interval(self): """Initializes with default check interval.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() assert processor.check_interval == 10 assert processor.running is False assert processor.thread is None def test_init_custom_interval(self): """Initializes with custom check interval.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2(check_interval=30) assert processor.check_interval == 30 def test_init_creates_empty_user_set(self): """Initializes with empty users to check set.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() assert len(processor._users_to_check) == 0 def test_init_creates_empty_pending_operations(self): """Initializes with empty pending operations dict.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() assert processor.pending_operations == {} class TestQueueProcessorV2StartStop: """Tests for start and stop methods.""" def test_start_sets_running_flag(self): """start sets running flag to True.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() with patch.object(processor, "_process_queue_loop"): processor.start() assert processor.running is True # Clean up processor.running = False if processor.thread: processor.thread.join(timeout=1) def test_start_creates_thread(self): """start creates a daemon thread.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() with patch.object(processor, "_process_queue_loop"): processor.start() assert processor.thread is not None assert processor.thread.daemon is True # Clean up processor.running = False processor.thread.join(timeout=1) def test_start_when_already_running(self): """start does nothing if already running.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor.running = True # Should not create a new thread processor.start() assert processor.thread is None def test_stop_sets_running_flag_false(self): """stop sets running flag to False.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor.running = True processor.thread = Mock() processor.thread.join = Mock() processor.stop() assert processor.running is False processor.thread.join.assert_called_once_with(timeout=10) def test_stop_interrupts_check_interval_wait(self): """stop returns promptly while the loop sleeps between iterations. The autouse reset_singletons fixture stops the processor after every app test, so a stop() that blocks for check_interval adds that many seconds of teardown to each of those tests (observed as uniform 10.00s teardowns that pushed CI past its job timeout). """ import time from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2(check_interval=30) # Wrap the real wait so the test knows when the loop has entered # it — deterministic, unlike sleeping and hoping the first # iteration has finished. loop_entered_wait = threading.Event() real_wait = processor._stop_event.wait def tracking_wait(timeout=None): loop_entered_wait.set() return real_wait(timeout) processor._stop_event.wait = tracking_wait processor.start() try: assert loop_entered_wait.wait(timeout=10), ( "loop never reached its inter-iteration wait" ) started = time.monotonic() processor.stop() elapsed = time.monotonic() - started assert elapsed < 5, ( f"stop() blocked for {elapsed:.1f}s; the loop's " "inter-iteration wait was not interrupted" ) assert not processor.thread.is_alive() finally: processor.stop() def test_start_after_stop_rearms_loop_wait(self): """A restarted processor loops again instead of exiting instantly. stop() leaves _stop_event set; start() must clear it or the restarted loop's wait returns immediately every iteration (busy-spin) — and create_app() does restart this singleton after the test-suite teardown stops it. """ from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor.start() processor.stop() with patch.object(processor, "_process_queue_loop"): processor.start() try: assert not processor._stop_event.is_set() finally: processor.stop() class TestQueueProcessorV2NotifyUserActivity: """Tests for notify_user_activity method.""" def test_notify_user_activity_adds_to_set(self): """notify_user_activity adds user to check set.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor.notify_user_activity("testuser", "session123") assert ("testuser", "session123") in processor._users_to_check def test_notify_user_activity_thread_safe(self): """notify_user_activity is thread safe.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() # Add from multiple threads threads = [] for i in range(10): t = threading.Thread( target=processor.notify_user_activity, args=(f"user{i}", f"session{i}"), ) threads.append(t) t.start() for t in threads: t.join() # All should be added assert len(processor._users_to_check) == 10 class TestQueueProcessorV2NotifyResearchQueued: """Tests for notify_research_queued method.""" @patch("local_deep_research.web.queue.processor_v2.get_user_db_session") def test_notify_research_queued_queues_task(self, mock_get_session): """notify_research_queued adds task to queue.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) mock_get_session.return_value = mock_session with patch( "local_deep_research.web.queue.processor_v2.UserQueueService" ) as mock_queue_service_class: mock_queue_service = Mock() mock_queue_service_class.return_value = mock_queue_service processor.notify_research_queued("testuser", "research123") mock_queue_service.add_task_metadata.assert_called_once() class TestQueueProcessorV2NotifyResearchCompleted: """Tests for notify_research_completed method.""" def test_notify_research_completed_removes_from_active(self): """notify_research_completed removes user from active set.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor._users_to_check.add("testuser:session123") # Complete should trigger next queue check processor.notify_research_completed("testuser", 123) # Method exists and doesn't crash class TestQueueProcessorV2QueueOperations: """Tests for queue operation methods.""" def test_queue_error_update_stores_pending(self): """queue_error_update stores pending operation.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor.queue_error_update( username="testuser", research_id="123", status="failed", error_message="Test error", metadata={"key": "value"}, completed_at="2024-01-01T00:00:00", ) # Should store in pending operations assert len(processor.pending_operations) > 0 # Check operation details operation = list(processor.pending_operations.values())[0] assert operation["operation_type"] == "error_update" assert operation["username"] == "testuser" assert operation["status"] == "failed" def test_queue_progress_update_stores_pending(self): """queue_progress_update stores pending operation.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() processor.queue_progress_update( username="testuser", research_id="123", progress=0.5, ) # Should store in pending operations assert len(processor.pending_operations) > 0 class TestQueueProcessorV2PendingOperations: """Tests for pending operations handling.""" def test_pending_operations_thread_safe(self): """Pending operations are thread safe.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() # Add operations from multiple threads using progress updates (simpler signature) threads = [] for i in range(10): t = threading.Thread( target=processor.queue_progress_update, kwargs={ "username": f"user{i}", "research_id": str(i), "progress": 0.5, }, ) threads.append(t) t.start() for t in threads: t.join() # All should be stored assert len(processor.pending_operations) == 10 class TestQueueProcessorV2DirectExecution: """Tests for direct execution mode.""" @patch("local_deep_research.web.queue.processor_v2.session_password_store") @patch("local_deep_research.web.queue.processor_v2.db_manager") @patch("local_deep_research.web.queue.processor_v2.get_user_db_session") def test_direct_execution_checks_queue_mode( self, mock_get_session, mock_db_manager, mock_password_store ): """Direct execution checks user's queue_mode setting.""" from local_deep_research.web.queue.processor_v2 import ( QueueProcessorV2, ) processor = QueueProcessorV2() mock_password_store.get_session_password.return_value = "password123" mock_db_manager.open_user_database.return_value = Mock() mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) mock_get_session.return_value = mock_session with patch( "local_deep_research.web.queue.processor_v2.UserQueueService" ) as mock_queue: mock_queue_instance = Mock() mock_queue.return_value = mock_queue_instance # Direct execution requires settings manager with patch( "local_deep_research.settings.manager.SettingsManager" ) as mock_settings: mock_settings_instance = Mock() mock_settings_instance.get_setting.side_effect = ( lambda key, default: ( "queue" if key == "app.queue_mode" else default ) ) mock_settings.return_value = mock_settings_instance processor.notify_research_queued( "testuser", "research123", session_id="session456", query="test query", ) # Should fall back to queue mode mock_queue_instance.add_task_metadata.assert_called_once()