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
398 lines
12 KiB
Python
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
|