""" Deep behavioral tests for BaseRecommender and recommendation patterns. Tests strategy naming, dependency injection, topic filtering, query generation, and recommendation data structures. """ from unittest.mock import Mock import pytest from local_deep_research.news.recommender.base_recommender import ( BaseRecommender, ) from local_deep_research.news.recommender.topic_based import ( TopicBasedRecommender, SearchBasedRecommender, ) # --- BaseRecommender abstract enforcement --- class TestBaseRecommenderAbstract: """Tests for BaseRecommender abstract patterns.""" def test_cannot_instantiate_directly(self): with pytest.raises(TypeError): BaseRecommender() def test_strategy_name_default(self): r = TopicBasedRecommender() assert r.strategy_name == "TopicBasedRecommender" def test_strategy_name_is_class_name(self): r = SearchBasedRecommender() assert r.strategy_name == "SearchBasedRecommender" # --- Dependency injection --- class TestRecommenderDependencyInjection: """Tests for recommender dependency injection.""" def test_preference_manager_default_none(self): r = TopicBasedRecommender() assert r.preference_manager is None def test_preference_manager_injected(self): pm = Mock() r = TopicBasedRecommender(preference_manager=pm) assert r.preference_manager is pm def test_topic_registry_default_none(self): r = TopicBasedRecommender() assert r.topic_registry is None def test_topic_registry_injected(self): tr = Mock() r = TopicBasedRecommender(topic_registry=tr) assert r.topic_registry is tr def test_search_system_default_none(self): r = TopicBasedRecommender() assert r.search_system is None def test_search_system_injected(self): ss = Mock() r = TopicBasedRecommender(search_system=ss) assert r.search_system is ss def test_rating_system_default_none(self): r = TopicBasedRecommender() assert r.rating_system is None def test_rating_system_injected(self): rs = Mock() r = TopicBasedRecommender(rating_system=rs) assert r.rating_system is rs def test_progress_callback_default_none(self): r = TopicBasedRecommender() assert r.progress_callback is None def test_set_progress_callback(self): r = TopicBasedRecommender() cb = Mock() r.set_progress_callback(cb) assert r.progress_callback is cb # --- max_recommendations --- class TestMaxRecommendations: """Tests for max_recommendations configuration.""" def test_default_max_recommendations(self): r = TopicBasedRecommender() assert r.max_recommendations == 5 # --- Default trending topics --- class TestDefaultTrendingTopics: """Tests for the default trending topics list.""" def test_returns_list(self): r = TopicBasedRecommender() topics = r._get_trending_topics(None) assert isinstance(topics, list) def test_has_ai_topic(self): r = TopicBasedRecommender() topics = r._get_trending_topics(None) assert any("artificial intelligence" in t for t in topics) def test_multiple_defaults(self): r = TopicBasedRecommender() topics = r._get_trending_topics(None) assert len(topics) >= 3 def test_context_topics_added(self): r = TopicBasedRecommender() context = {"current_news_topics": ["Custom Topic"]} topics = r._get_trending_topics(context) assert "Custom Topic" in topics def test_has_cybersecurity(self): r = TopicBasedRecommender() topics = r._get_trending_topics(None) assert any("cybersecurity" in t for t in topics) def test_has_climate(self): r = TopicBasedRecommender() topics = r._get_trending_topics(None) assert any("climate" in t for t in topics) # --- Query generation --- class TestQueryGeneration: """Tests for topic query generation.""" def test_query_contains_topic(self): r = TopicBasedRecommender() query = r._generate_topic_query("climate change") assert "climate change" in query def test_query_has_news_context(self): r = TopicBasedRecommender() query = r._generate_topic_query("AI") lower = query.lower() assert "news" in lower def test_query_has_latest(self): r = TopicBasedRecommender() query = r._generate_topic_query("AI") assert "latest" in query.lower() def test_query_has_breaking(self): r = TopicBasedRecommender() query = r._generate_topic_query("AI") assert "breaking" in query.lower() def test_different_topics_different_queries(self): r = TopicBasedRecommender() q1 = r._generate_topic_query("AI") q2 = r._generate_topic_query("Climate") assert q1 != q2 def test_empty_topic(self): r = TopicBasedRecommender() query = r._generate_topic_query("") assert "news" in query.lower() # --- SearchBasedRecommender --- class TestSearchBasedRecommenderBehavior: """Tests for SearchBasedRecommender behavior.""" def test_always_returns_empty(self): r = SearchBasedRecommender() assert r.generate_recommendations("user1") == [] def test_with_context_returns_empty(self): r = SearchBasedRecommender() assert r.generate_recommendations("user1", context={"key": "val"}) == [] def test_strategy_name(self): r = SearchBasedRecommender() assert r.strategy_name == "SearchBasedRecommender" def test_inherits_from_base(self): r = SearchBasedRecommender() assert isinstance(r, BaseRecommender) # --- Filter topics by preferences --- class TestFilterTopicsByPreferences: """Tests for _filter_topics_by_preferences.""" def test_no_preferences_returns_all(self): r = TopicBasedRecommender() topics = ["AI News", "Climate Change"] result = r._filter_topics_by_preferences(topics, {}) assert len(result) == 2 def test_disliked_removes_matches(self): r = TopicBasedRecommender() topics = ["Sports Update", "AI News"] prefs = {"disliked_topics": ["sports"]} result = r._filter_topics_by_preferences(topics, prefs) assert "Sports Update" not in result assert "AI News" in result def test_disliked_case_insensitive(self): r = TopicBasedRecommender() topics = ["SPORTS update"] prefs = {"disliked_topics": ["sports"]} result = r._filter_topics_by_preferences(topics, prefs) assert len(result) == 0 def test_interest_boost_reorders(self): r = TopicBasedRecommender() topics = ["Regular Topic", "Super AI Topic"] prefs = {"interests": {"ai": 10.0}} result = r._filter_topics_by_preferences(topics, prefs) assert result[0] == "Super AI Topic" def test_negative_weight_interest(self): r = TopicBasedRecommender() topics = ["AI News", "Climate Change"] prefs = {"interests": {"ai": -1.0}} result = r._filter_topics_by_preferences(topics, prefs) # Negative weight should still keep the topic assert "AI News" in result def test_zero_weight_interest(self): r = TopicBasedRecommender() topics = ["AI News", "Climate Change"] prefs = {"interests": {"ai": 0.0}} result = r._filter_topics_by_preferences(topics, prefs) assert len(result) == 2 def test_very_large_weight(self): r = TopicBasedRecommender() topics = ["Regular Topic", "Super Topic"] prefs = {"interests": {"super": 999.0}} result = r._filter_topics_by_preferences(topics, prefs) assert result[0] == "Super Topic" def test_single_topic(self): r = TopicBasedRecommender() topics = ["AI"] result = r._filter_topics_by_preferences(topics, {}) assert result == ["AI"] def test_disliked_removes_all_matches(self): r = TopicBasedRecommender() topics = ["Sports Update", "Sports Score", "Sports Analysis"] prefs = {"disliked_topics": ["sports"]} result = r._filter_topics_by_preferences(topics, prefs) assert len(result) == 0 def test_unicode_topics(self): r = TopicBasedRecommender() topics = ["AI研究", "技術ニュース"] result = r._filter_topics_by_preferences(topics, {}) assert len(result) == 2 def test_empty_topics_list(self): r = TopicBasedRecommender() result = r._filter_topics_by_preferences([], {}) assert result == [] # --- Trending topics with registry --- class TestTrendingWithRegistry: """Tests for trending topic retrieval with registry.""" def test_registry_results_used(self): registry = Mock() registry.get_trending_topics.return_value = ["Registry A", "Registry B"] r = TopicBasedRecommender(topic_registry=registry) topics = r._get_trending_topics(None) assert "Registry A" in topics def test_empty_registry_gets_defaults(self): registry = Mock() registry.get_trending_topics.return_value = [] r = TopicBasedRecommender(topic_registry=registry) topics = r._get_trending_topics(None) assert len(topics) > 0 def test_context_and_registry_combined(self): registry = Mock() registry.get_trending_topics.return_value = ["From Registry"] r = TopicBasedRecommender(topic_registry=registry) context = {"current_news_topics": ["From Context"]} topics = r._get_trending_topics(context) assert "From Registry" in topics assert "From Context" in topics def test_none_context_and_none_registry(self): r = TopicBasedRecommender() topics = r._get_trending_topics(None) assert isinstance(topics, list) assert len(topics) > 0 # --- Get user preferences --- class TestGetUserPreferences: """Tests for _get_user_preferences helper.""" def test_returns_empty_without_manager(self): r = TopicBasedRecommender() prefs = r._get_user_preferences("user1") assert prefs == {} def test_delegates_to_manager(self): pm = Mock() pm.get_preferences.return_value = {"key": "val"} r = TopicBasedRecommender(preference_manager=pm) prefs = r._get_user_preferences("user1") assert prefs == {"key": "val"} pm.get_preferences.assert_called_once_with("user1") # --- Get user ratings --- class TestGetUserRatings: """Tests for _get_user_ratings helper.""" def test_returns_empty_without_system(self): r = TopicBasedRecommender() ratings = r._get_user_ratings("user1") assert ratings == [] def test_delegates_to_rating_system(self): rs = Mock() rs.get_recent_ratings.return_value = [{"id": 1}] r = TopicBasedRecommender(rating_system=rs) ratings = r._get_user_ratings("user1") assert ratings == [{"id": 1}] # --- Execute search --- class TestExecuteSearch: """Tests for _execute_search helper.""" def test_returns_error_without_system(self): r = TopicBasedRecommender() result = r._execute_search("query") assert "error" in result def test_delegates_to_search_system(self): ss = Mock() ss.analyze_topic.return_value = {"results": []} r = TopicBasedRecommender(search_system=ss) result = r._execute_search("query") assert result == {"results": []} def test_handles_search_exception(self): ss = Mock() ss.analyze_topic.side_effect = Exception("search failed") r = TopicBasedRecommender(search_system=ss) result = r._execute_search("query") assert "error" in result # --- Strategy info --- class TestStrategyInfo: """Tests for get_strategy_info.""" def test_has_name(self): r = TopicBasedRecommender() info = r.get_strategy_info() assert info["name"] == "TopicBasedRecommender" def test_reports_no_preference_manager(self): r = TopicBasedRecommender() info = r.get_strategy_info() assert info["has_preference_manager"] is False def test_reports_preference_manager_present(self): r = TopicBasedRecommender(preference_manager=Mock()) info = r.get_strategy_info() assert info["has_preference_manager"] is True def test_reports_no_search_system(self): r = TopicBasedRecommender() info = r.get_strategy_info() assert info["has_search_system"] is False def test_has_description(self): r = TopicBasedRecommender() info = r.get_strategy_info() assert "description" in info