Files
learningcircuit--local-deep…/tests/news/test_preference_topic_registry_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

427 lines
13 KiB
Python

"""
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