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
653 lines
22 KiB
Python
653 lines
22 KiB
Python
"""
|
|
Extended tests for news/recommender/topic_based.py
|
|
|
|
Tests cover:
|
|
- TopicBasedRecommender initialization
|
|
- generate_recommendations() method
|
|
- _get_trending_topics() method
|
|
- _filter_topics_by_preferences() method
|
|
- _generate_topic_query() method
|
|
- _create_recommendation_card() method
|
|
- SearchBasedRecommender class
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
class TestTopicBasedRecommenderInit:
|
|
"""Tests for TopicBasedRecommender initialization."""
|
|
|
|
def test_creates_instance(self):
|
|
"""Creates TopicBasedRecommender instance."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
assert recommender is not None
|
|
|
|
def test_inherits_from_base_recommender(self):
|
|
"""Inherits from BaseRecommender."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
from local_deep_research.news.recommender.base_recommender import (
|
|
BaseRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
assert isinstance(recommender, BaseRecommender)
|
|
|
|
def test_default_max_recommendations(self):
|
|
"""Default max_recommendations is 5."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
assert recommender.max_recommendations == 5
|
|
|
|
def test_accepts_topic_registry(self):
|
|
"""Accepts topic_registry parameter."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
mock_registry = Mock()
|
|
recommender = TopicBasedRecommender(topic_registry=mock_registry)
|
|
|
|
assert recommender.topic_registry is mock_registry
|
|
|
|
def test_accepts_preference_manager(self):
|
|
"""Accepts preference_manager parameter."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
mock_manager = Mock()
|
|
recommender = TopicBasedRecommender(preference_manager=mock_manager)
|
|
|
|
assert recommender.preference_manager is mock_manager
|
|
|
|
|
|
class TestGenerateRecommendations:
|
|
"""Tests for generate_recommendations() method."""
|
|
|
|
def test_returns_list(self):
|
|
"""Returns a list."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender._get_trending_topics = Mock(return_value=["AI"])
|
|
recommender._filter_topics_by_preferences = Mock(return_value=[])
|
|
recommender._get_user_preferences = Mock(return_value={})
|
|
|
|
result = recommender.generate_recommendations("user1")
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_calls_get_trending_topics(self):
|
|
"""Calls _get_trending_topics."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender._get_trending_topics = Mock(return_value=[])
|
|
recommender._filter_topics_by_preferences = Mock(return_value=[])
|
|
recommender._get_user_preferences = Mock(return_value={})
|
|
|
|
recommender.generate_recommendations("user1")
|
|
|
|
recommender._get_trending_topics.assert_called_once()
|
|
|
|
def test_calls_filter_topics_by_preferences(self):
|
|
"""Calls _filter_topics_by_preferences."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender._get_trending_topics = Mock(return_value=["AI"])
|
|
recommender._filter_topics_by_preferences = Mock(return_value=[])
|
|
recommender._get_user_preferences = Mock(return_value={})
|
|
|
|
recommender.generate_recommendations("user1")
|
|
|
|
recommender._filter_topics_by_preferences.assert_called_once()
|
|
|
|
def test_limits_to_max_recommendations(self):
|
|
"""Limits topics to max_recommendations."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender.max_recommendations = 2
|
|
recommender._get_trending_topics = Mock(
|
|
return_value=["A", "B", "C", "D", "E"]
|
|
)
|
|
recommender._filter_topics_by_preferences = Mock(
|
|
return_value=["A", "B", "C", "D", "E"]
|
|
)
|
|
recommender._get_user_preferences = Mock(return_value={})
|
|
recommender._create_recommendation_card = Mock(return_value=None)
|
|
|
|
recommender.generate_recommendations("user1")
|
|
|
|
# Should only process max_recommendations topics
|
|
assert recommender._create_recommendation_card.call_count <= 2
|
|
|
|
def test_handles_empty_topics(self):
|
|
"""Handles empty topics list."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender._get_trending_topics = Mock(return_value=[])
|
|
recommender._filter_topics_by_preferences = Mock(return_value=[])
|
|
recommender._get_user_preferences = Mock(return_value={})
|
|
|
|
result = recommender.generate_recommendations("user1")
|
|
|
|
assert result == []
|
|
|
|
def test_handles_exception_gracefully(self):
|
|
"""Handles exception gracefully."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender._get_trending_topics = Mock(side_effect=Exception("Error"))
|
|
|
|
result = recommender.generate_recommendations("user1")
|
|
|
|
# Should return empty list, not raise
|
|
assert result == []
|
|
|
|
|
|
class TestGetTrendingTopics:
|
|
"""Tests for _get_trending_topics() method."""
|
|
|
|
def test_returns_list(self):
|
|
"""Returns a list."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._get_trending_topics(None)
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_uses_topic_registry_if_available(self):
|
|
"""Uses topic_registry if available."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
mock_registry = Mock()
|
|
mock_registry.get_trending_topics.return_value = ["AI", "ML"]
|
|
|
|
recommender = TopicBasedRecommender(topic_registry=mock_registry)
|
|
result = recommender._get_trending_topics(None)
|
|
|
|
mock_registry.get_trending_topics.assert_called_once()
|
|
assert "AI" in result
|
|
|
|
def test_adds_context_topics(self):
|
|
"""Adds topics from context."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
context = {"current_news_topics": ["Technology"]}
|
|
result = recommender._get_trending_topics(context)
|
|
|
|
assert "Technology" in result
|
|
|
|
def test_returns_fallback_topics_when_none_found(self):
|
|
"""Returns fallback topics when none found."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._get_trending_topics(None)
|
|
|
|
# Should have fallback topics
|
|
assert len(result) > 0
|
|
|
|
def test_fallback_includes_ai_topic(self):
|
|
"""Fallback includes AI topic."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._get_trending_topics(None)
|
|
|
|
assert any("artificial intelligence" in t.lower() for t in result)
|
|
|
|
|
|
class TestFilterTopicsByPreferences:
|
|
"""Tests for _filter_topics_by_preferences() method."""
|
|
|
|
def test_returns_list(self):
|
|
"""Returns a list."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
topics = ["AI", "Sports"]
|
|
preferences = {}
|
|
|
|
result = recommender._filter_topics_by_preferences(topics, preferences)
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_filters_disliked_topics(self):
|
|
"""Filters out disliked topics."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
topics = ["AI News", "Sports News", "Tech Updates"]
|
|
preferences = {"disliked_topics": ["sports"]}
|
|
|
|
result = recommender._filter_topics_by_preferences(topics, preferences)
|
|
|
|
assert "Sports News" not in result
|
|
assert "AI News" in result
|
|
|
|
def test_boosts_topics_matching_interests(self):
|
|
"""Boosts topics matching interests."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
topics = ["General News", "AI Developments"]
|
|
preferences = {"interests": {"AI": 2.0}}
|
|
|
|
result = recommender._filter_topics_by_preferences(topics, preferences)
|
|
|
|
# AI topic should be first due to higher boost
|
|
assert result[0] == "AI Developments"
|
|
|
|
def test_preserves_all_topics_when_no_disliked(self):
|
|
"""Preserves all topics when no disliked topics."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
topics = ["AI", "Tech", "Science"]
|
|
preferences = {}
|
|
|
|
result = recommender._filter_topics_by_preferences(topics, preferences)
|
|
|
|
assert len(result) == 3
|
|
|
|
def test_handles_empty_topics_list(self):
|
|
"""Handles empty topics list."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._filter_topics_by_preferences([], {})
|
|
|
|
assert result == []
|
|
|
|
def test_case_insensitive_dislike_matching(self):
|
|
"""Case insensitive dislike matching."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
topics = ["SPORTS News", "sports update"]
|
|
preferences = {"disliked_topics": ["Sports"]}
|
|
|
|
result = recommender._filter_topics_by_preferences(topics, preferences)
|
|
|
|
assert len(result) == 0
|
|
|
|
|
|
class TestGenerateTopicQuery:
|
|
"""Tests for _generate_topic_query() method."""
|
|
|
|
def test_returns_string(self):
|
|
"""Returns a string."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._generate_topic_query("AI")
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_includes_topic_in_query(self):
|
|
"""Includes topic in query."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._generate_topic_query("artificial intelligence")
|
|
|
|
assert "artificial intelligence" in result
|
|
|
|
def test_includes_news_keywords(self):
|
|
"""Includes news-related keywords."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._generate_topic_query("tech")
|
|
|
|
assert "news" in result.lower()
|
|
|
|
def test_includes_latest_keyword(self):
|
|
"""Includes 'latest' keyword for fresh news."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._generate_topic_query("topic")
|
|
|
|
assert "latest" in result.lower()
|
|
|
|
|
|
class TestCreateRecommendationCard:
|
|
"""Tests for _create_recommendation_card() method."""
|
|
|
|
def test_returns_none_on_search_error(self):
|
|
"""Returns None when search fails."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
|
|
) as MockSearch:
|
|
mock_instance = Mock()
|
|
mock_instance.analyze_topic.return_value = {
|
|
"error": "Search failed"
|
|
}
|
|
MockSearch.return_value = mock_instance
|
|
|
|
recommender = TopicBasedRecommender()
|
|
result = recommender._create_recommendation_card(
|
|
"topic", "query", "user1"
|
|
)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_when_no_items_found(self):
|
|
"""Returns None when no news items found."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
|
|
) as MockSearch:
|
|
mock_instance = Mock()
|
|
mock_instance.analyze_topic.return_value = {"news_items": []}
|
|
MockSearch.return_value = mock_instance
|
|
|
|
recommender = TopicBasedRecommender()
|
|
result = recommender._create_recommendation_card(
|
|
"topic", "query", "user1"
|
|
)
|
|
|
|
assert result is None
|
|
|
|
def test_handles_exception_gracefully(self):
|
|
"""Handles exception gracefully."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
|
|
) as MockSearch:
|
|
MockSearch.side_effect = Exception("Search system error")
|
|
|
|
recommender = TopicBasedRecommender()
|
|
result = recommender._create_recommendation_card(
|
|
"topic", "query", "user1"
|
|
)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_when_llm_not_configured(self):
|
|
"""Returns None when get_llm() raises ValueError; does NOT proceed to construct
|
|
AdvancedSearchSystem. Scheduler runs repeatedly — must not propagate stack trace.
|
|
|
|
Strengthened to distinguish the specific `except ValueError` path from the
|
|
outer `except Exception` swallow: this asserts that the *warning* log was
|
|
emitted (specific path) rather than `logger.exception` (catch-all path).
|
|
Without this assertion, removing the targeted `except ValueError` block
|
|
would still let the test pass — the outer handler would also return None.
|
|
"""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
from local_deep_research.security import sanitize_for_log
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.config.llm_config.get_llm"
|
|
) as mock_get_llm,
|
|
patch(
|
|
"local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
|
|
) as MockSearch,
|
|
patch(
|
|
"local_deep_research.news.recommender.topic_based.logger"
|
|
) as mock_logger,
|
|
):
|
|
mock_get_llm.side_effect = ValueError(
|
|
"LLM model not configured. ..."
|
|
)
|
|
|
|
# Topic carries a CRLF log-injection payload; the warning must
|
|
# interpolate the *sanitized* form. Binding it to a variable (and
|
|
# deriving the expected substring from it) removes the previous
|
|
# assertion's coupling to a hard-coded "AI" literal.
|
|
topic = "AI\r\ninjected-fake-log-line"
|
|
recommender = TopicBasedRecommender()
|
|
result = recommender._create_recommendation_card(
|
|
topic, "query", "user1"
|
|
)
|
|
|
|
assert result is None
|
|
mock_get_llm.assert_called_once()
|
|
# Critical: we returned before building the search system.
|
|
MockSearch.assert_not_called()
|
|
# The targeted except-ValueError path uses logger.warning; the
|
|
# catch-all `except Exception` would use logger.exception. Asserting
|
|
# warning was called and exception was NOT pins the code path.
|
|
mock_logger.warning.assert_called_once()
|
|
warning_msg = mock_logger.warning.call_args[0][0]
|
|
assert "LLM not configured" in warning_msg
|
|
# The sanitized topic is interpolated; the raw CR/LF payload never
|
|
# reaches the log line (this is the log-injection guarantee).
|
|
assert sanitize_for_log(topic) in warning_msg
|
|
assert "\r" not in warning_msg
|
|
assert "\n" not in warning_msg
|
|
mock_logger.exception.assert_not_called()
|
|
|
|
|
|
class TestSearchBasedRecommender:
|
|
"""Tests for SearchBasedRecommender class."""
|
|
|
|
def test_creates_instance(self):
|
|
"""Creates SearchBasedRecommender instance."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
SearchBasedRecommender,
|
|
)
|
|
|
|
recommender = SearchBasedRecommender()
|
|
|
|
assert recommender is not None
|
|
|
|
def test_inherits_from_base_recommender(self):
|
|
"""Inherits from BaseRecommender."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
SearchBasedRecommender,
|
|
)
|
|
from local_deep_research.news.recommender.base_recommender import (
|
|
BaseRecommender,
|
|
)
|
|
|
|
recommender = SearchBasedRecommender()
|
|
|
|
assert isinstance(recommender, BaseRecommender)
|
|
|
|
def test_generate_recommendations_returns_list(self):
|
|
"""generate_recommendations returns a list."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
SearchBasedRecommender,
|
|
)
|
|
|
|
recommender = SearchBasedRecommender()
|
|
|
|
result = recommender.generate_recommendations("user1")
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_returns_empty_list_when_tracking_disabled(self):
|
|
"""Returns empty list when search tracking is disabled."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
SearchBasedRecommender,
|
|
)
|
|
|
|
recommender = SearchBasedRecommender()
|
|
|
|
result = recommender.generate_recommendations("user1")
|
|
|
|
# Currently returns empty since tracking is off by default
|
|
assert result == []
|
|
|
|
def test_accepts_context(self):
|
|
"""Accepts context parameter."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
SearchBasedRecommender,
|
|
)
|
|
|
|
recommender = SearchBasedRecommender()
|
|
|
|
result = recommender.generate_recommendations(
|
|
"user1", context={"key": "value"}
|
|
)
|
|
|
|
assert isinstance(result, list)
|
|
|
|
|
|
class TestRecommenderProgressTracking:
|
|
"""Tests for progress tracking in recommenders."""
|
|
|
|
def test_has_progress_callback_support(self):
|
|
"""Recommender supports progress callback."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
# Should have progress_callback attribute
|
|
assert hasattr(recommender, "progress_callback")
|
|
|
|
def test_set_progress_callback(self):
|
|
"""Can set progress callback."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
callback = Mock()
|
|
recommender.set_progress_callback(callback)
|
|
|
|
assert recommender.progress_callback is callback
|
|
|
|
|
|
class TestRecommenderEdgeCases:
|
|
"""Edge case tests for recommenders."""
|
|
|
|
def test_empty_user_id(self):
|
|
"""Handles empty user_id."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
recommender._get_trending_topics = Mock(return_value=[])
|
|
recommender._filter_topics_by_preferences = Mock(return_value=[])
|
|
recommender._get_user_preferences = Mock(return_value={})
|
|
|
|
result = recommender.generate_recommendations("")
|
|
|
|
assert isinstance(result, list)
|
|
|
|
def test_unicode_topic(self):
|
|
"""Handles unicode topics."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._generate_topic_query("日本語ニュース")
|
|
|
|
assert "日本語ニュース" in result
|
|
|
|
def test_very_long_topic(self):
|
|
"""Handles very long topics."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
long_topic = "A" * 1000
|
|
|
|
result = recommender._generate_topic_query(long_topic)
|
|
|
|
assert long_topic in result
|
|
|
|
def test_special_characters_in_topic(self):
|
|
"""Handles special characters in topics."""
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
)
|
|
|
|
recommender = TopicBasedRecommender()
|
|
|
|
result = recommender._generate_topic_query("C++ & Python")
|
|
|
|
assert "C++ & Python" in result
|