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
14 KiB
Python
416 lines
14 KiB
Python
"""
|
|
Tests for database/queue_service.py
|
|
|
|
Tests cover:
|
|
- UserQueueService initialization
|
|
- Queue status management
|
|
- Task metadata operations
|
|
- Task status updates
|
|
- Pending task retrieval
|
|
- Task cleanup
|
|
"""
|
|
|
|
from unittest.mock import Mock
|
|
from datetime import datetime, UTC
|
|
|
|
|
|
class TestUserQueueServiceInit:
|
|
"""Tests for UserQueueService initialization."""
|
|
|
|
def test_init_with_session(self):
|
|
"""Test initialization with a session."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
service = UserQueueService(mock_session)
|
|
|
|
assert service.session == mock_session
|
|
|
|
|
|
class TestUpdateQueueStatus:
|
|
"""Tests for update_queue_status method."""
|
|
|
|
def test_updates_existing_status(self):
|
|
"""Test updating existing queue status."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_status = Mock()
|
|
mock_session.query.return_value.first.return_value = mock_status
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.update_queue_status(5, 10, "task-123")
|
|
|
|
assert mock_status.active_tasks == 5
|
|
assert mock_status.queued_tasks == 10
|
|
assert mock_status.last_task_id == "task-123"
|
|
mock_session.commit.assert_called_once()
|
|
|
|
def test_creates_new_status_when_none_exists(self):
|
|
"""Test creating status when none exists."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_session.query.return_value.first.return_value = None
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.update_queue_status(2, 5)
|
|
|
|
mock_session.add.assert_called_once()
|
|
mock_session.commit.assert_called_once()
|
|
|
|
|
|
class TestGetQueueStatus:
|
|
"""Tests for get_queue_status method."""
|
|
|
|
def test_returns_status_dict(self):
|
|
"""Test returns status as dictionary."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_status = Mock()
|
|
mock_status.active_tasks = 3
|
|
mock_status.queued_tasks = 7
|
|
mock_status.last_checked = datetime.now(UTC)
|
|
mock_status.last_task_id = "task-456"
|
|
mock_session.query.return_value.first.return_value = mock_status
|
|
|
|
service = UserQueueService(mock_session)
|
|
result = service.get_queue_status()
|
|
|
|
assert result["active_tasks"] == 3
|
|
assert result["queued_tasks"] == 7
|
|
assert result["last_task_id"] == "task-456"
|
|
|
|
def test_returns_none_when_no_status(self):
|
|
"""Test returns None when no status exists."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_session.query.return_value.first.return_value = None
|
|
|
|
service = UserQueueService(mock_session)
|
|
result = service.get_queue_status()
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestAddTaskMetadata:
|
|
"""Tests for add_task_metadata method."""
|
|
|
|
def test_adds_task_metadata(self):
|
|
"""Test adding task metadata."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
# Mock for _increment_queue_count
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 0
|
|
mock_session.query.return_value.first.return_value = mock_status
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.add_task_metadata("task-1", "research", priority=5)
|
|
|
|
mock_session.add.assert_called()
|
|
mock_session.commit.assert_called()
|
|
|
|
def test_increments_queue_count(self):
|
|
"""Test that queue count is incremented."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 2
|
|
mock_session.query.return_value.first.return_value = mock_status
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.add_task_metadata("task-1", "research")
|
|
|
|
assert mock_status.queued_tasks == 3
|
|
|
|
|
|
class TestUpdateTaskStatus:
|
|
"""Tests for update_task_status method."""
|
|
|
|
def test_updates_task_to_processing(self):
|
|
"""Test updating task to processing status."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_task = Mock()
|
|
mock_task.status = "queued"
|
|
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 5
|
|
mock_status.active_tasks = 2
|
|
|
|
# Set up query chain
|
|
mock_filter = Mock()
|
|
mock_filter.first.return_value = mock_task
|
|
mock_query = Mock()
|
|
mock_query.filter_by.return_value = mock_filter
|
|
mock_query.first.return_value = mock_status
|
|
|
|
def query_side_effect(model):
|
|
if hasattr(model, "task_id"): # TaskMetadata
|
|
return mock_query
|
|
return Mock(first=Mock(return_value=mock_status))
|
|
|
|
mock_session.query.side_effect = query_side_effect
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.update_task_status("task-1", "processing")
|
|
|
|
assert mock_task.status == "processing"
|
|
|
|
def test_updates_task_to_completed(self):
|
|
"""Test updating task to completed status."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_task = Mock()
|
|
mock_task.status = "processing"
|
|
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 3
|
|
mock_status.active_tasks = 2
|
|
|
|
mock_filter = Mock()
|
|
mock_filter.first.return_value = mock_task
|
|
mock_query = Mock()
|
|
mock_query.filter_by.return_value = mock_filter
|
|
mock_query.first.return_value = mock_status
|
|
|
|
def query_side_effect(model):
|
|
if hasattr(model, "task_id"):
|
|
return mock_query
|
|
return Mock(first=Mock(return_value=mock_status))
|
|
|
|
mock_session.query.side_effect = query_side_effect
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.update_task_status("task-1", "completed")
|
|
|
|
assert mock_task.status == "completed"
|
|
|
|
def test_updates_task_with_error(self):
|
|
"""Test updating task with error message."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_task = Mock()
|
|
mock_task.status = "processing"
|
|
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 0
|
|
mock_status.active_tasks = 1
|
|
|
|
mock_filter = Mock()
|
|
mock_filter.first.return_value = mock_task
|
|
mock_query = Mock()
|
|
mock_query.filter_by.return_value = mock_filter
|
|
mock_query.first.return_value = mock_status
|
|
|
|
def query_side_effect(model):
|
|
if hasattr(model, "task_id"):
|
|
return mock_query
|
|
return Mock(first=Mock(return_value=mock_status))
|
|
|
|
mock_session.query.side_effect = query_side_effect
|
|
|
|
service = UserQueueService(mock_session)
|
|
service.update_task_status("task-1", "failed", "Something went wrong")
|
|
|
|
assert mock_task.status == "failed"
|
|
assert mock_task.error_message == "Something went wrong"
|
|
|
|
def test_handles_nonexistent_task(self):
|
|
"""Test handling when task doesn't exist."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_filter = Mock()
|
|
mock_filter.first.return_value = None
|
|
mock_query = Mock()
|
|
mock_query.filter_by.return_value = mock_filter
|
|
mock_session.query.return_value = mock_query
|
|
|
|
service = UserQueueService(mock_session)
|
|
# Should not raise
|
|
service.update_task_status("nonexistent", "completed")
|
|
|
|
|
|
class TestGetPendingTasks:
|
|
"""Tests for get_pending_tasks method."""
|
|
|
|
def test_returns_pending_tasks(self):
|
|
"""Test getting pending tasks."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_task1 = Mock()
|
|
mock_task1.task_id = "task-1"
|
|
mock_task1.task_type = "research"
|
|
mock_task1.created_at = datetime.now(UTC)
|
|
mock_task1.priority = 5
|
|
|
|
mock_task2 = Mock()
|
|
mock_task2.task_id = "task-2"
|
|
mock_task2.task_type = "analysis"
|
|
mock_task2.created_at = datetime.now(UTC)
|
|
mock_task2.priority = 3
|
|
|
|
mock_query = mock_session.query.return_value
|
|
mock_query.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = [
|
|
mock_task1,
|
|
mock_task2,
|
|
]
|
|
|
|
service = UserQueueService(mock_session)
|
|
result = service.get_pending_tasks(limit=5)
|
|
|
|
assert len(result) == 2
|
|
assert result[0]["task_id"] == "task-1"
|
|
assert result[0]["task_type"] == "research"
|
|
assert result[1]["task_id"] == "task-2"
|
|
|
|
def test_returns_empty_list_when_no_tasks(self):
|
|
"""Test returns empty list when no pending tasks."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_query = mock_session.query.return_value
|
|
mock_query.filter_by.return_value.order_by.return_value.limit.return_value.all.return_value = []
|
|
|
|
service = UserQueueService(mock_session)
|
|
result = service.get_pending_tasks()
|
|
|
|
assert result == []
|
|
|
|
|
|
class TestCleanupOldTasks:
|
|
"""Tests for cleanup_old_tasks method."""
|
|
|
|
def test_deletes_old_tasks(self):
|
|
"""Test deleting old completed tasks."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_query = mock_session.query.return_value
|
|
mock_query.filter.return_value.delete.return_value = 5
|
|
|
|
service = UserQueueService(mock_session)
|
|
result = service.cleanup_old_tasks(days=7)
|
|
|
|
assert result == 5
|
|
mock_session.commit.assert_called_once()
|
|
|
|
|
|
class TestQueueCountHelpers:
|
|
"""Tests for queue count helper methods."""
|
|
|
|
def test_increment_queue_count_existing(self):
|
|
"""Test incrementing queue count with existing status."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 5
|
|
mock_session.query.return_value.first.return_value = mock_status
|
|
|
|
service = UserQueueService(mock_session)
|
|
service._increment_queue_count()
|
|
|
|
assert mock_status.queued_tasks == 6
|
|
|
|
def test_increment_queue_count_new_status(self):
|
|
"""Test incrementing queue count creates new status."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_session.query.return_value.first.return_value = None
|
|
|
|
service = UserQueueService(mock_session)
|
|
service._increment_queue_count()
|
|
|
|
mock_session.add.assert_called_once()
|
|
|
|
def test_update_queue_counts_clamps_to_zero(self):
|
|
"""Test that queue counts don't go below zero."""
|
|
from local_deep_research.database.queue_service import UserQueueService
|
|
|
|
mock_session = Mock()
|
|
mock_status = Mock()
|
|
mock_status.queued_tasks = 1
|
|
mock_status.active_tasks = 1
|
|
mock_session.query.return_value.first.return_value = mock_status
|
|
|
|
service = UserQueueService(mock_session)
|
|
service._update_queue_counts(-5, -5)
|
|
|
|
assert mock_status.queued_tasks == 0
|
|
assert mock_status.active_tasks == 0
|
|
|
|
|
|
class TestProcessingToQueuedRevert:
|
|
"""update_task_status('queued') after 'processing' reverts the counter.
|
|
|
|
Regression: the queued-dispatch loop's SystemAtCapacityError branch
|
|
re-queues a research without reverting the queued->processing counter
|
|
claim, so each capacity-rejected retry leaked a slot into active_tasks
|
|
and decremented queued_tasks. Under sustained capacity pressure
|
|
queued_tasks drifts to 0, at which point _process_user_queue treats the
|
|
queue as empty and stops dispatching the still-present rows. These use
|
|
a real in-memory DB so the counter arithmetic is actually exercised.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _service():
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from local_deep_research.database.models.queue import (
|
|
QueueStatus,
|
|
TaskMetadata,
|
|
)
|
|
from local_deep_research.database.queue_service import (
|
|
UserQueueService,
|
|
)
|
|
|
|
engine = create_engine("sqlite:///:memory:")
|
|
QueueStatus.__table__.create(engine)
|
|
TaskMetadata.__table__.create(engine)
|
|
session = sessionmaker(bind=engine)()
|
|
return UserQueueService(session)
|
|
|
|
def test_processing_then_queued_round_trips_counts(self):
|
|
svc = self._service()
|
|
svc.add_task_metadata("r1", "research") # queued 1 / active 0
|
|
assert svc.get_queue_status()["queued_tasks"] == 1
|
|
assert svc.get_queue_status()["active_tasks"] == 0
|
|
|
|
svc.update_task_status("r1", "processing") # queued 0 / active 1
|
|
s = svc.get_queue_status()
|
|
assert s["queued_tasks"] == 0
|
|
assert s["active_tasks"] == 1
|
|
|
|
svc.update_task_status("r1", "queued") # reverted: queued 1 / active 0
|
|
s = svc.get_queue_status()
|
|
assert s["queued_tasks"] == 1, (
|
|
"queued->processing claim must be reverted on re-queue"
|
|
)
|
|
assert s["active_tasks"] == 0, (
|
|
"active slot must not leak after a capacity re-queue"
|
|
)
|
|
|
|
def test_repeated_capacity_rejects_do_not_drift(self):
|
|
"""Three processing->queued cycles (simulating capacity retries)
|
|
must leave the counter exactly where it started."""
|
|
svc = self._service()
|
|
svc.add_task_metadata("r1", "research")
|
|
for _ in range(3):
|
|
svc.update_task_status("r1", "processing")
|
|
svc.update_task_status("r1", "queued")
|
|
s = svc.get_queue_status()
|
|
assert s["queued_tasks"] == 1
|
|
assert s["active_tasks"] == 0
|