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
427 lines
13 KiB
Python
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
|