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
416 lines
13 KiB
Python
416 lines
13 KiB
Python
"""
|
|
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()
|