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
399 lines
14 KiB
Python
399 lines
14 KiB
Python
"""Tests for metrics search_tracker module."""
|
|
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
|
|
from local_deep_research.metrics.search_tracker import (
|
|
SearchTracker,
|
|
get_search_tracker,
|
|
)
|
|
|
|
|
|
class TestSearchTrackerInit:
|
|
"""Tests for SearchTracker initialization."""
|
|
|
|
def test_initializes_with_default_database(self):
|
|
"""Should initialize with default MetricsDatabase."""
|
|
tracker = SearchTracker()
|
|
assert tracker.db is not None
|
|
|
|
def test_initializes_with_custom_database(self):
|
|
"""Should use provided database."""
|
|
mock_db = MagicMock()
|
|
tracker = SearchTracker(db=mock_db)
|
|
assert tracker.db is mock_db
|
|
|
|
|
|
class TestSearchTrackerRecordSearch:
|
|
"""Tests for record_search static method."""
|
|
|
|
def test_extracts_context_correctly(self, mock_search_context):
|
|
"""Should extract research context from thread context."""
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_writer = MagicMock()
|
|
mock_writer.get_session.return_value = mock_cm
|
|
|
|
with patch(
|
|
"local_deep_research.metrics.search_tracker.get_search_context",
|
|
return_value=mock_search_context,
|
|
):
|
|
with patch(
|
|
"local_deep_research.database.thread_metrics.metrics_writer",
|
|
mock_writer,
|
|
):
|
|
SearchTracker.record_search(
|
|
engine_name="brave",
|
|
query="test query",
|
|
results_count=5,
|
|
response_time_ms=150,
|
|
)
|
|
|
|
# Should have added a SearchCall record
|
|
mock_session.add.assert_called_once()
|
|
|
|
def test_handles_integer_research_id(self, mock_search_context):
|
|
"""Should convert integer research_id to string."""
|
|
mock_search_context["research_id"] = 12345 # Integer
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_writer = MagicMock()
|
|
mock_writer.get_session.return_value = mock_cm
|
|
|
|
with patch(
|
|
"local_deep_research.metrics.search_tracker.get_search_context",
|
|
return_value=mock_search_context,
|
|
):
|
|
with patch(
|
|
"local_deep_research.database.thread_metrics.metrics_writer",
|
|
mock_writer,
|
|
):
|
|
SearchTracker.record_search(
|
|
engine_name="duckduckgo",
|
|
query="test",
|
|
)
|
|
|
|
# Should have converted to string
|
|
call_args = mock_session.add.call_args[0][0]
|
|
assert call_args.research_id == "12345"
|
|
|
|
def test_sets_success_status_for_successful_search(
|
|
self, mock_search_context
|
|
):
|
|
"""Should set success_status to 'success' when success=True."""
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_writer = MagicMock()
|
|
mock_writer.get_session.return_value = mock_cm
|
|
|
|
with patch(
|
|
"local_deep_research.metrics.search_tracker.get_search_context",
|
|
return_value=mock_search_context,
|
|
):
|
|
with patch(
|
|
"local_deep_research.database.thread_metrics.metrics_writer",
|
|
mock_writer,
|
|
):
|
|
SearchTracker.record_search(
|
|
engine_name="google",
|
|
query="test",
|
|
success=True,
|
|
)
|
|
|
|
call_args = mock_session.add.call_args[0][0]
|
|
assert call_args.success_status == "success"
|
|
|
|
def test_sets_error_status_for_failed_search(self, mock_search_context):
|
|
"""Should set success_status to 'error' when success=False."""
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_writer = MagicMock()
|
|
mock_writer.get_session.return_value = mock_cm
|
|
|
|
with patch(
|
|
"local_deep_research.metrics.search_tracker.get_search_context",
|
|
return_value=mock_search_context,
|
|
):
|
|
with patch(
|
|
"local_deep_research.database.thread_metrics.metrics_writer",
|
|
mock_writer,
|
|
):
|
|
SearchTracker.record_search(
|
|
engine_name="google",
|
|
query="test",
|
|
success=False,
|
|
error_message="Connection timeout",
|
|
)
|
|
|
|
call_args = mock_session.add.call_args[0][0]
|
|
assert call_args.success_status == "error"
|
|
assert call_args.error_message == "Connection timeout"
|
|
|
|
|
|
class TestSearchTrackerGetSearchMetrics:
|
|
"""Tests for get_search_metrics method."""
|
|
|
|
def test_applies_time_filter(self):
|
|
"""Should apply time filter to query."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.group_by.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.limit.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
tracker.get_search_metrics(period="7d")
|
|
|
|
# Should have called filter (for time)
|
|
assert mock_query.filter.called
|
|
|
|
def test_applies_mode_filter(self):
|
|
"""Should apply research mode filter."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.group_by.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.limit.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
tracker.get_search_metrics(research_mode="quick")
|
|
|
|
# Should have applied filters
|
|
assert mock_query.filter.called
|
|
|
|
def test_returns_engine_stats_structure(self):
|
|
"""Should return search_engine_stats and recent_calls."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.group_by.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.limit.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
result = tracker.get_search_metrics()
|
|
|
|
assert "search_engine_stats" in result
|
|
assert "recent_calls" in result
|
|
assert isinstance(result["search_engine_stats"], list)
|
|
assert isinstance(result["recent_calls"], list)
|
|
|
|
def test_handles_database_error(self):
|
|
"""Should handle database errors gracefully."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
mock_session.query.side_effect = Exception("DB Error")
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
result = tracker.get_search_metrics()
|
|
|
|
assert result == {"search_engine_stats": [], "recent_calls": []}
|
|
|
|
|
|
class TestSearchTrackerGetResearchSearchMetrics:
|
|
"""Tests for get_research_search_metrics method."""
|
|
|
|
def test_filters_by_research_id(self):
|
|
"""Should filter results by research_id."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.group_by.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
tracker.get_research_search_metrics("test-uuid-123")
|
|
|
|
# Should have called filter with research_id
|
|
assert mock_query.filter.called
|
|
|
|
def test_passes_username_to_session(self):
|
|
"""Should pass username and password to db.get_session()."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.group_by.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
tracker.get_research_search_metrics(
|
|
"test-uuid", username="alice", password="secret"
|
|
)
|
|
|
|
mock_db.get_session.assert_called_once_with(
|
|
username="alice", password="secret"
|
|
)
|
|
|
|
def test_returns_metrics_structure(self):
|
|
"""Should return expected metrics structure."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.group_by.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
result = tracker.get_research_search_metrics("test-uuid")
|
|
|
|
assert "total_searches" in result
|
|
assert "total_results" in result
|
|
assert "avg_response_time" in result
|
|
assert "success_rate" in result
|
|
assert "search_calls" in result
|
|
assert "engine_stats" in result
|
|
|
|
|
|
class TestSearchTrackerGetSearchTimeSeries:
|
|
"""Tests for get_search_time_series method."""
|
|
|
|
def test_returns_ordered_data(self):
|
|
"""Should return time series data ordered by timestamp."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
result = tracker.get_search_time_series()
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_applies_filters(self):
|
|
"""Should apply time and mode filters."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
# Setup query chain
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
tracker.get_search_time_series(period="30d", research_mode="detailed")
|
|
|
|
# Should have applied filters
|
|
assert mock_query.filter.called
|
|
|
|
def test_passes_username_to_session(self):
|
|
"""Should pass username and password to db.get_session()."""
|
|
mock_db = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__ = Mock(return_value=mock_session)
|
|
mock_cm.__exit__ = Mock(return_value=None)
|
|
mock_db.get_session.return_value = mock_cm
|
|
|
|
mock_query = MagicMock()
|
|
mock_session.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.order_by.return_value = mock_query
|
|
mock_query.all.return_value = []
|
|
|
|
tracker = SearchTracker(db=mock_db)
|
|
tracker.get_search_time_series(username="bob", password="pw123")
|
|
|
|
mock_db.get_session.assert_called_once_with(
|
|
username="bob", password="pw123"
|
|
)
|
|
|
|
|
|
class TestGetSearchTracker:
|
|
"""Tests for get_search_tracker factory function."""
|
|
|
|
def test_returns_search_tracker_instance(self):
|
|
"""Should return a SearchTracker instance."""
|
|
tracker = get_search_tracker()
|
|
assert isinstance(tracker, SearchTracker)
|
|
|
|
def test_returns_fresh_instance_each_call(self):
|
|
"""Should return a new instance on each call (no singleton)."""
|
|
first = get_search_tracker()
|
|
second = get_search_tracker()
|
|
|
|
assert isinstance(first, SearchTracker)
|
|
assert isinstance(second, SearchTracker)
|
|
assert first is not second
|