""" Deep behavioral tests for BasePreferenceManager and TopicRegistry. Tests default preferences, interest management, topic ignore, source boosting, and topic registry trending calculations. """ from unittest.mock import Mock import pytest from local_deep_research.news.preference_manager.base_preference import ( BasePreferenceManager, TopicRegistry, ) # --- BasePreferenceManager abstract --- class TestBasePreferenceManagerAbstract: """Tests for BasePreferenceManager abstract enforcement.""" def test_cannot_instantiate_directly(self): with pytest.raises(TypeError): BasePreferenceManager() def test_storage_backend_default_none(self): """Concrete subclass should have storage_backend=None by default.""" class ConcretePrefs(BasePreferenceManager): def get_preferences(self, user_id): return {} def update_preferences(self, user_id, preferences): return preferences mgr = ConcretePrefs() assert mgr.storage_backend is None def test_storage_backend_injected(self): class ConcretePrefs(BasePreferenceManager): def get_preferences(self, user_id): return {} def update_preferences(self, user_id, preferences): return preferences backend = Mock() mgr = ConcretePrefs(storage_backend=backend) assert mgr.storage_backend is backend # --- Concrete preference manager for testing --- class InMemoryPreferenceManager(BasePreferenceManager): """In-memory implementation for testing.""" def __init__(self): super().__init__() self._store = {} def get_preferences(self, user_id): return self._store.get(user_id, self.get_default_preferences()) def update_preferences(self, user_id, preferences): self._store[user_id] = preferences return preferences # --- Default preferences --- class TestDefaultPreferences: """Tests for get_default_preferences.""" def test_has_liked_categories(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert "liked_categories" in prefs assert prefs["liked_categories"] == [] def test_has_disliked_categories(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["disliked_categories"] == [] def test_has_liked_topics(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["liked_topics"] == [] def test_has_disliked_topics(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["disliked_topics"] == [] def test_has_interests(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["interests"] == {} def test_has_source_weights(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["source_weights"] == {} def test_has_impact_threshold(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["impact_threshold"] == 5 def test_has_focus_preferences(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() focus = prefs["focus_preferences"] assert focus["surprising"] is False assert focus["breaking"] is True assert focus["positive"] is False assert focus["local"] is False def test_has_custom_search_terms(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["custom_search_terms"] == "" def test_has_search_strategy(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert prefs["search_strategy"] == "news_aggregation" def test_has_created_at(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert "created_at" in prefs def test_has_preferences_updated_at(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_default_preferences() assert "preferences_updated_at" in prefs # --- Interest management --- class TestInterestManagement: """Tests for add_interest and remove_interest.""" def test_add_interest(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI", 1.5) prefs = mgr.get_preferences("u1") assert prefs["interests"]["AI"] == 1.5 def test_add_interest_default_weight(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "ML") prefs = mgr.get_preferences("u1") assert prefs["interests"]["ML"] == 1.0 def test_add_multiple_interests(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI", 2.0) mgr.add_interest("u1", "ML", 1.5) prefs = mgr.get_preferences("u1") assert len(prefs["interests"]) == 2 def test_overwrite_interest_weight(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI", 1.0) mgr.add_interest("u1", "AI", 3.0) prefs = mgr.get_preferences("u1") assert prefs["interests"]["AI"] == 3.0 def test_remove_interest(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI", 1.0) mgr.remove_interest("u1", "AI") prefs = mgr.get_preferences("u1") assert "AI" not in prefs["interests"] def test_remove_nonexistent_interest(self): mgr = InMemoryPreferenceManager() # Should not raise mgr.remove_interest("u1", "nonexistent") def test_add_interest_sets_timestamp(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI") prefs = mgr.get_preferences("u1") assert "interests_updated_at" in prefs def test_remove_interest_sets_timestamp(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI") mgr.remove_interest("u1", "AI") prefs = mgr.get_preferences("u1") assert "interests_updated_at" in prefs # --- Topic ignore --- class TestTopicIgnore: """Tests for ignore_topic.""" def test_ignore_topic_adds_to_list(self): mgr = InMemoryPreferenceManager() mgr.ignore_topic("u1", "politics") prefs = mgr.get_preferences("u1") assert "politics" in prefs["disliked_topics"] def test_ignore_topic_no_duplicates(self): mgr = InMemoryPreferenceManager() mgr.ignore_topic("u1", "sports") mgr.ignore_topic("u1", "sports") prefs = mgr.get_preferences("u1") assert prefs["disliked_topics"].count("sports") == 1 def test_ignore_multiple_topics(self): mgr = InMemoryPreferenceManager() mgr.ignore_topic("u1", "sports") mgr.ignore_topic("u1", "politics") prefs = mgr.get_preferences("u1") assert len(prefs["disliked_topics"]) == 2 def test_ignore_sets_timestamp(self): mgr = InMemoryPreferenceManager() mgr.ignore_topic("u1", "sports") prefs = mgr.get_preferences("u1") assert "preferences_updated_at" in prefs # --- Source boosting --- class TestSourceBoosting: """Tests for boost_source.""" def test_boost_source(self): mgr = InMemoryPreferenceManager() mgr.boost_source("u1", "reuters.com", 2.0) prefs = mgr.get_preferences("u1") assert prefs["source_weights"]["reuters.com"] == 2.0 def test_boost_default_weight(self): mgr = InMemoryPreferenceManager() mgr.boost_source("u1", "bbc.com") prefs = mgr.get_preferences("u1") assert prefs["source_weights"]["bbc.com"] == 1.5 def test_boost_multiple_sources(self): mgr = InMemoryPreferenceManager() mgr.boost_source("u1", "reuters.com", 2.0) mgr.boost_source("u1", "bbc.com", 1.8) prefs = mgr.get_preferences("u1") assert len(prefs["source_weights"]) == 2 def test_overwrite_source_weight(self): mgr = InMemoryPreferenceManager() mgr.boost_source("u1", "reuters.com", 1.5) mgr.boost_source("u1", "reuters.com", 3.0) prefs = mgr.get_preferences("u1") assert prefs["source_weights"]["reuters.com"] == 3.0 # --- User isolation --- class TestPreferenceUserIsolation: """Tests that preferences are isolated between users.""" def test_different_users_different_prefs(self): mgr = InMemoryPreferenceManager() mgr.add_interest("u1", "AI", 2.0) mgr.add_interest("u2", "Sports", 1.5) u1_prefs = mgr.get_preferences("u1") u2_prefs = mgr.get_preferences("u2") assert "AI" in u1_prefs["interests"] assert "AI" not in u2_prefs["interests"] assert "Sports" in u2_prefs["interests"] def test_new_user_gets_defaults(self): mgr = InMemoryPreferenceManager() prefs = mgr.get_preferences("new_user") defaults = mgr.get_default_preferences() assert prefs["impact_threshold"] == defaults["impact_threshold"] # --- TopicRegistry --- class TestTopicRegistryInit: """Tests for TopicRegistry initialization.""" def test_default_no_llm(self): reg = TopicRegistry() assert reg.llm_client is None def test_llm_injected(self): llm = Mock() reg = TopicRegistry(llm_client=llm) assert reg.llm_client is llm def test_initial_topics_empty(self): reg = TopicRegistry() assert reg.topics == {} # --- Topic registration --- class TestTopicRegistration: """Tests for register_topic.""" def test_register_new_topic(self): reg = TopicRegistry() reg.register_topic("AI") assert "AI" in reg.topics def test_register_sets_count_1(self): reg = TopicRegistry() reg.register_topic("AI") assert reg.topics["AI"]["count"] == 1 def test_register_increments_count(self): reg = TopicRegistry() reg.register_topic("AI") reg.register_topic("AI") assert reg.topics["AI"]["count"] == 2 def test_register_sets_first_seen(self): reg = TopicRegistry() reg.register_topic("AI") assert reg.topics["AI"]["first_seen"] is not None def test_register_sets_last_seen(self): reg = TopicRegistry() reg.register_topic("AI") assert reg.topics["AI"]["last_seen"] is not None def test_register_multiple_topics(self): reg = TopicRegistry() reg.register_topic("AI") reg.register_topic("ML") reg.register_topic("NLP") assert len(reg.topics) == 3 # --- Trending topics --- class TestTrendingTopics: """Tests for get_trending_topics.""" def test_empty_registry_returns_empty(self): reg = TopicRegistry() assert reg.get_trending_topics() == [] def test_recent_topics_returned(self): reg = TopicRegistry() reg.register_topic("AI") topics = reg.get_trending_topics(hours=24) assert "AI" in topics def test_ordered_by_count(self): reg = TopicRegistry() reg.register_topic("less popular") for _ in range(5): reg.register_topic("popular") topics = reg.get_trending_topics() assert topics[0] == "popular" def test_respects_limit(self): reg = TopicRegistry() for i in range(20): reg.register_topic(f"topic-{i}") topics = reg.get_trending_topics(limit=5) assert len(topics) == 5 def test_default_limit_10(self): reg = TopicRegistry() for i in range(15): reg.register_topic(f"topic-{i}") topics = reg.get_trending_topics() assert len(topics) == 10 # --- Topic info --- class TestTopicInfo: """Tests for get_topic_info.""" def test_returns_info_for_registered(self): reg = TopicRegistry() reg.register_topic("AI") info = reg.get_topic_info("AI") assert info is not None assert "count" in info def test_returns_none_for_unknown(self): reg = TopicRegistry() assert reg.get_topic_info("unknown") is None def test_count_matches_registrations(self): reg = TopicRegistry() for _ in range(7): reg.register_topic("AI") info = reg.get_topic_info("AI") assert info["count"] == 7 def test_first_seen_stable(self): reg = TopicRegistry() reg.register_topic("AI") first_seen = reg.topics["AI"]["first_seen"] reg.register_topic("AI") assert reg.topics["AI"]["first_seen"] == first_seen def test_last_seen_updates(self): reg = TopicRegistry() reg.register_topic("AI") first_last_seen = reg.topics["AI"]["last_seen"] reg.register_topic("AI") # last_seen should be >= first registration assert reg.topics["AI"]["last_seen"] >= first_last_seen