Files
learningcircuit--local-deep…/tests/news/test_preference_manager_behavior.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

398 lines
12 KiB
Python

"""
Deep behavioral tests for BasePreferenceManager and TopicRegistry.
Tests interest management, topic ignore, source boosting, default preferences,
topic registration, and trending topic calculation.
"""
from datetime import timedelta
from unittest.mock import Mock
from local_deep_research.news.preference_manager.base_preference import (
BasePreferenceManager,
TopicRegistry,
)
from local_deep_research.news.core.utils import utc_now
# --- Concrete implementation for testing ---
class ConcretePreferenceManager(BasePreferenceManager):
"""Concrete implementation for testing the abstract base class."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
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
# --- BasePreferenceManager init ---
class TestBasePreferenceManagerInit:
"""Tests for BasePreferenceManager initialization."""
def test_stores_storage_backend(self):
backend = Mock()
pm = ConcretePreferenceManager(storage_backend=backend)
assert pm.storage_backend is backend
def test_storage_backend_none_by_default(self):
pm = ConcretePreferenceManager()
assert pm.storage_backend is None
# --- get_default_preferences ---
class TestGetDefaultPreferences:
"""Tests for default preferences structure."""
def test_has_liked_categories(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["liked_categories"] == []
def test_has_disliked_categories(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["disliked_categories"] == []
def test_has_liked_topics(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["liked_topics"] == []
def test_has_disliked_topics(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["disliked_topics"] == []
def test_has_interests_dict(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["interests"] == {}
def test_has_source_weights(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["source_weights"] == {}
def test_has_impact_threshold(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["impact_threshold"] == 5
def test_has_focus_preferences(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
focus = prefs["focus_preferences"]
assert focus["breaking"] is True
assert focus["surprising"] is False
assert focus["positive"] is False
assert focus["local"] is False
def test_has_custom_search_terms(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["custom_search_terms"] == ""
def test_has_search_strategy(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert prefs["search_strategy"] == "news_aggregation"
def test_has_created_at(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert "created_at" in prefs
def test_has_preferences_updated_at(self):
pm = ConcretePreferenceManager()
prefs = pm.get_default_preferences()
assert "preferences_updated_at" in prefs
# --- add_interest ---
class TestAddInterest:
"""Tests for adding interests."""
def test_adds_interest(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI")
prefs = pm.get_preferences("user1")
assert "AI" in prefs["interests"]
def test_default_weight_is_1(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI")
prefs = pm.get_preferences("user1")
assert prefs["interests"]["AI"] == 1.0
def test_custom_weight(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI", weight=3.5)
prefs = pm.get_preferences("user1")
assert prefs["interests"]["AI"] == 3.5
def test_multiple_interests(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI")
pm.add_interest("user1", "Climate")
prefs = pm.get_preferences("user1")
assert "AI" in prefs["interests"]
assert "Climate" in prefs["interests"]
def test_overwrites_existing_interest(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI", weight=1.0)
pm.add_interest("user1", "AI", weight=5.0)
prefs = pm.get_preferences("user1")
assert prefs["interests"]["AI"] == 5.0
def test_sets_interests_updated_at(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI")
prefs = pm.get_preferences("user1")
assert "interests_updated_at" in prefs
# --- remove_interest ---
class TestRemoveInterest:
"""Tests for removing interests."""
def test_removes_existing_interest(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI")
pm.remove_interest("user1", "AI")
prefs = pm.get_preferences("user1")
assert "AI" not in prefs["interests"]
def test_removes_nothing_if_not_found(self):
pm = ConcretePreferenceManager()
pm.add_interest("user1", "AI")
pm.remove_interest("user1", "Nonexistent")
prefs = pm.get_preferences("user1")
assert "AI" in prefs["interests"]
def test_removes_nothing_if_no_interests(self):
pm = ConcretePreferenceManager()
pm.remove_interest("user1", "AI") # Should not raise
# --- ignore_topic ---
class TestIgnoreTopic:
"""Tests for ignoring topics."""
def test_adds_to_disliked_topics(self):
pm = ConcretePreferenceManager()
pm.ignore_topic("user1", "Sports")
prefs = pm.get_preferences("user1")
assert "Sports" in prefs["disliked_topics"]
def test_no_duplicates(self):
pm = ConcretePreferenceManager()
pm.ignore_topic("user1", "Sports")
pm.ignore_topic("user1", "Sports")
prefs = pm.get_preferences("user1")
assert prefs["disliked_topics"].count("Sports") == 1
def test_multiple_topics(self):
pm = ConcretePreferenceManager()
pm.ignore_topic("user1", "Sports")
pm.ignore_topic("user1", "Politics")
prefs = pm.get_preferences("user1")
assert "Sports" in prefs["disliked_topics"]
assert "Politics" in prefs["disliked_topics"]
def test_sets_preferences_updated_at(self):
pm = ConcretePreferenceManager()
pm.ignore_topic("user1", "Sports")
prefs = pm.get_preferences("user1")
assert "preferences_updated_at" in prefs
# --- boost_source ---
class TestBoostSource:
"""Tests for source boosting."""
def test_adds_source_weight(self):
pm = ConcretePreferenceManager()
pm.boost_source("user1", "reuters.com")
prefs = pm.get_preferences("user1")
assert "reuters.com" in prefs["source_weights"]
def test_default_weight_is_1_5(self):
pm = ConcretePreferenceManager()
pm.boost_source("user1", "reuters.com")
prefs = pm.get_preferences("user1")
assert prefs["source_weights"]["reuters.com"] == 1.5
def test_custom_weight(self):
pm = ConcretePreferenceManager()
pm.boost_source("user1", "reuters.com", weight=2.0)
prefs = pm.get_preferences("user1")
assert prefs["source_weights"]["reuters.com"] == 2.0
def test_overwrites_existing_source(self):
pm = ConcretePreferenceManager()
pm.boost_source("user1", "reuters.com", weight=1.5)
pm.boost_source("user1", "reuters.com", weight=3.0)
prefs = pm.get_preferences("user1")
assert prefs["source_weights"]["reuters.com"] == 3.0
# --- TopicRegistry ---
class TestTopicRegistryInit:
"""Tests for TopicRegistry initialization."""
def test_empty_topics_initially(self):
registry = TopicRegistry()
assert registry.topics == {}
def test_stores_llm_client(self):
client = Mock()
registry = TopicRegistry(llm_client=client)
assert registry.llm_client is client
def test_llm_client_none_by_default(self):
registry = TopicRegistry()
assert registry.llm_client is None
# --- register_topic ---
class TestRegisterTopic:
"""Tests for topic registration."""
def test_registers_new_topic(self):
registry = TopicRegistry()
registry.register_topic("AI")
assert "AI" in registry.topics
def test_sets_first_seen(self):
registry = TopicRegistry()
registry.register_topic("AI")
assert "first_seen" in registry.topics["AI"]
def test_sets_count_to_1(self):
registry = TopicRegistry()
registry.register_topic("AI")
assert registry.topics["AI"]["count"] == 1
def test_increments_count(self):
registry = TopicRegistry()
registry.register_topic("AI")
registry.register_topic("AI")
registry.register_topic("AI")
assert registry.topics["AI"]["count"] == 3
def test_updates_last_seen(self):
registry = TopicRegistry()
registry.register_topic("AI")
first_seen = registry.topics["AI"]["last_seen"]
registry.register_topic("AI")
assert registry.topics["AI"]["last_seen"] >= first_seen
def test_first_seen_unchanged_on_reregister(self):
registry = TopicRegistry()
registry.register_topic("AI")
first_seen = registry.topics["AI"]["first_seen"]
registry.register_topic("AI")
assert registry.topics["AI"]["first_seen"] == first_seen
# --- get_trending_topics ---
class TestGetTrendingTopics:
"""Tests for trending topic retrieval."""
def test_empty_registry(self):
registry = TopicRegistry()
assert registry.get_trending_topics() == []
def test_returns_recent_topics(self):
registry = TopicRegistry()
registry.register_topic("AI")
registry.register_topic("Climate")
result = registry.get_trending_topics(hours=24)
assert "AI" in result
assert "Climate" in result
def test_sorted_by_count(self):
registry = TopicRegistry()
registry.register_topic("Rare")
for _ in range(5):
registry.register_topic("Popular")
result = registry.get_trending_topics()
assert result[0] == "Popular"
def test_respects_limit(self):
registry = TopicRegistry()
for i in range(20):
registry.register_topic(f"Topic {i}")
result = registry.get_trending_topics(limit=5)
assert len(result) == 5
def test_filters_old_topics(self):
registry = TopicRegistry()
registry.register_topic("Old Topic")
# Manually set last_seen to be old
registry.topics["Old Topic"]["last_seen"] = utc_now() - timedelta(
hours=48
)
result = registry.get_trending_topics(hours=24)
assert "Old Topic" not in result
def test_default_limit_is_10(self):
registry = TopicRegistry()
for i in range(15):
registry.register_topic(f"Topic {i}")
result = registry.get_trending_topics()
assert len(result) == 10
# --- get_topic_info ---
class TestGetTopicInfo:
"""Tests for topic information retrieval."""
def test_returns_none_for_unknown(self):
registry = TopicRegistry()
assert registry.get_topic_info("Unknown") is None
def test_returns_info_for_known(self):
registry = TopicRegistry()
registry.register_topic("AI")
info = registry.get_topic_info("AI")
assert info is not None
assert "first_seen" in info
assert "count" in info
assert "last_seen" in info
def test_count_reflects_registrations(self):
registry = TopicRegistry()
registry.register_topic("AI")
registry.register_topic("AI")
info = registry.get_topic_info("AI")
assert info["count"] == 2