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
415 lines
13 KiB
Python
415 lines
13 KiB
Python
"""
|
|
Deep behavioral tests for BaseRecommender and recommendation patterns.
|
|
Tests strategy naming, dependency injection, topic filtering,
|
|
query generation, and recommendation data structures.
|
|
"""
|
|
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.news.recommender.base_recommender import (
|
|
BaseRecommender,
|
|
)
|
|
from local_deep_research.news.recommender.topic_based import (
|
|
TopicBasedRecommender,
|
|
SearchBasedRecommender,
|
|
)
|
|
|
|
|
|
# --- BaseRecommender abstract enforcement ---
|
|
|
|
|
|
class TestBaseRecommenderAbstract:
|
|
"""Tests for BaseRecommender abstract patterns."""
|
|
|
|
def test_cannot_instantiate_directly(self):
|
|
with pytest.raises(TypeError):
|
|
BaseRecommender()
|
|
|
|
def test_strategy_name_default(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.strategy_name == "TopicBasedRecommender"
|
|
|
|
def test_strategy_name_is_class_name(self):
|
|
r = SearchBasedRecommender()
|
|
assert r.strategy_name == "SearchBasedRecommender"
|
|
|
|
|
|
# --- Dependency injection ---
|
|
|
|
|
|
class TestRecommenderDependencyInjection:
|
|
"""Tests for recommender dependency injection."""
|
|
|
|
def test_preference_manager_default_none(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.preference_manager is None
|
|
|
|
def test_preference_manager_injected(self):
|
|
pm = Mock()
|
|
r = TopicBasedRecommender(preference_manager=pm)
|
|
assert r.preference_manager is pm
|
|
|
|
def test_topic_registry_default_none(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.topic_registry is None
|
|
|
|
def test_topic_registry_injected(self):
|
|
tr = Mock()
|
|
r = TopicBasedRecommender(topic_registry=tr)
|
|
assert r.topic_registry is tr
|
|
|
|
def test_search_system_default_none(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.search_system is None
|
|
|
|
def test_search_system_injected(self):
|
|
ss = Mock()
|
|
r = TopicBasedRecommender(search_system=ss)
|
|
assert r.search_system is ss
|
|
|
|
def test_rating_system_default_none(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.rating_system is None
|
|
|
|
def test_rating_system_injected(self):
|
|
rs = Mock()
|
|
r = TopicBasedRecommender(rating_system=rs)
|
|
assert r.rating_system is rs
|
|
|
|
def test_progress_callback_default_none(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.progress_callback is None
|
|
|
|
def test_set_progress_callback(self):
|
|
r = TopicBasedRecommender()
|
|
cb = Mock()
|
|
r.set_progress_callback(cb)
|
|
assert r.progress_callback is cb
|
|
|
|
|
|
# --- max_recommendations ---
|
|
|
|
|
|
class TestMaxRecommendations:
|
|
"""Tests for max_recommendations configuration."""
|
|
|
|
def test_default_max_recommendations(self):
|
|
r = TopicBasedRecommender()
|
|
assert r.max_recommendations == 5
|
|
|
|
|
|
# --- Default trending topics ---
|
|
|
|
|
|
class TestDefaultTrendingTopics:
|
|
"""Tests for the default trending topics list."""
|
|
|
|
def test_returns_list(self):
|
|
r = TopicBasedRecommender()
|
|
topics = r._get_trending_topics(None)
|
|
assert isinstance(topics, list)
|
|
|
|
def test_has_ai_topic(self):
|
|
r = TopicBasedRecommender()
|
|
topics = r._get_trending_topics(None)
|
|
assert any("artificial intelligence" in t for t in topics)
|
|
|
|
def test_multiple_defaults(self):
|
|
r = TopicBasedRecommender()
|
|
topics = r._get_trending_topics(None)
|
|
assert len(topics) >= 3
|
|
|
|
def test_context_topics_added(self):
|
|
r = TopicBasedRecommender()
|
|
context = {"current_news_topics": ["Custom Topic"]}
|
|
topics = r._get_trending_topics(context)
|
|
assert "Custom Topic" in topics
|
|
|
|
def test_has_cybersecurity(self):
|
|
r = TopicBasedRecommender()
|
|
topics = r._get_trending_topics(None)
|
|
assert any("cybersecurity" in t for t in topics)
|
|
|
|
def test_has_climate(self):
|
|
r = TopicBasedRecommender()
|
|
topics = r._get_trending_topics(None)
|
|
assert any("climate" in t for t in topics)
|
|
|
|
|
|
# --- Query generation ---
|
|
|
|
|
|
class TestQueryGeneration:
|
|
"""Tests for topic query generation."""
|
|
|
|
def test_query_contains_topic(self):
|
|
r = TopicBasedRecommender()
|
|
query = r._generate_topic_query("climate change")
|
|
assert "climate change" in query
|
|
|
|
def test_query_has_news_context(self):
|
|
r = TopicBasedRecommender()
|
|
query = r._generate_topic_query("AI")
|
|
lower = query.lower()
|
|
assert "news" in lower
|
|
|
|
def test_query_has_latest(self):
|
|
r = TopicBasedRecommender()
|
|
query = r._generate_topic_query("AI")
|
|
assert "latest" in query.lower()
|
|
|
|
def test_query_has_breaking(self):
|
|
r = TopicBasedRecommender()
|
|
query = r._generate_topic_query("AI")
|
|
assert "breaking" in query.lower()
|
|
|
|
def test_different_topics_different_queries(self):
|
|
r = TopicBasedRecommender()
|
|
q1 = r._generate_topic_query("AI")
|
|
q2 = r._generate_topic_query("Climate")
|
|
assert q1 != q2
|
|
|
|
def test_empty_topic(self):
|
|
r = TopicBasedRecommender()
|
|
query = r._generate_topic_query("")
|
|
assert "news" in query.lower()
|
|
|
|
|
|
# --- SearchBasedRecommender ---
|
|
|
|
|
|
class TestSearchBasedRecommenderBehavior:
|
|
"""Tests for SearchBasedRecommender behavior."""
|
|
|
|
def test_always_returns_empty(self):
|
|
r = SearchBasedRecommender()
|
|
assert r.generate_recommendations("user1") == []
|
|
|
|
def test_with_context_returns_empty(self):
|
|
r = SearchBasedRecommender()
|
|
assert r.generate_recommendations("user1", context={"key": "val"}) == []
|
|
|
|
def test_strategy_name(self):
|
|
r = SearchBasedRecommender()
|
|
assert r.strategy_name == "SearchBasedRecommender"
|
|
|
|
def test_inherits_from_base(self):
|
|
r = SearchBasedRecommender()
|
|
assert isinstance(r, BaseRecommender)
|
|
|
|
|
|
# --- Filter topics by preferences ---
|
|
|
|
|
|
class TestFilterTopicsByPreferences:
|
|
"""Tests for _filter_topics_by_preferences."""
|
|
|
|
def test_no_preferences_returns_all(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["AI News", "Climate Change"]
|
|
result = r._filter_topics_by_preferences(topics, {})
|
|
assert len(result) == 2
|
|
|
|
def test_disliked_removes_matches(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["Sports Update", "AI News"]
|
|
prefs = {"disliked_topics": ["sports"]}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
assert "Sports Update" not in result
|
|
assert "AI News" in result
|
|
|
|
def test_disliked_case_insensitive(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["SPORTS update"]
|
|
prefs = {"disliked_topics": ["sports"]}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
assert len(result) == 0
|
|
|
|
def test_interest_boost_reorders(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["Regular Topic", "Super AI Topic"]
|
|
prefs = {"interests": {"ai": 10.0}}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
assert result[0] == "Super AI Topic"
|
|
|
|
def test_negative_weight_interest(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["AI News", "Climate Change"]
|
|
prefs = {"interests": {"ai": -1.0}}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
# Negative weight should still keep the topic
|
|
assert "AI News" in result
|
|
|
|
def test_zero_weight_interest(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["AI News", "Climate Change"]
|
|
prefs = {"interests": {"ai": 0.0}}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
assert len(result) == 2
|
|
|
|
def test_very_large_weight(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["Regular Topic", "Super Topic"]
|
|
prefs = {"interests": {"super": 999.0}}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
assert result[0] == "Super Topic"
|
|
|
|
def test_single_topic(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["AI"]
|
|
result = r._filter_topics_by_preferences(topics, {})
|
|
assert result == ["AI"]
|
|
|
|
def test_disliked_removes_all_matches(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["Sports Update", "Sports Score", "Sports Analysis"]
|
|
prefs = {"disliked_topics": ["sports"]}
|
|
result = r._filter_topics_by_preferences(topics, prefs)
|
|
assert len(result) == 0
|
|
|
|
def test_unicode_topics(self):
|
|
r = TopicBasedRecommender()
|
|
topics = ["AI研究", "技術ニュース"]
|
|
result = r._filter_topics_by_preferences(topics, {})
|
|
assert len(result) == 2
|
|
|
|
def test_empty_topics_list(self):
|
|
r = TopicBasedRecommender()
|
|
result = r._filter_topics_by_preferences([], {})
|
|
assert result == []
|
|
|
|
|
|
# --- Trending topics with registry ---
|
|
|
|
|
|
class TestTrendingWithRegistry:
|
|
"""Tests for trending topic retrieval with registry."""
|
|
|
|
def test_registry_results_used(self):
|
|
registry = Mock()
|
|
registry.get_trending_topics.return_value = ["Registry A", "Registry B"]
|
|
r = TopicBasedRecommender(topic_registry=registry)
|
|
topics = r._get_trending_topics(None)
|
|
assert "Registry A" in topics
|
|
|
|
def test_empty_registry_gets_defaults(self):
|
|
registry = Mock()
|
|
registry.get_trending_topics.return_value = []
|
|
r = TopicBasedRecommender(topic_registry=registry)
|
|
topics = r._get_trending_topics(None)
|
|
assert len(topics) > 0
|
|
|
|
def test_context_and_registry_combined(self):
|
|
registry = Mock()
|
|
registry.get_trending_topics.return_value = ["From Registry"]
|
|
r = TopicBasedRecommender(topic_registry=registry)
|
|
context = {"current_news_topics": ["From Context"]}
|
|
topics = r._get_trending_topics(context)
|
|
assert "From Registry" in topics
|
|
assert "From Context" in topics
|
|
|
|
def test_none_context_and_none_registry(self):
|
|
r = TopicBasedRecommender()
|
|
topics = r._get_trending_topics(None)
|
|
assert isinstance(topics, list)
|
|
assert len(topics) > 0
|
|
|
|
|
|
# --- Get user preferences ---
|
|
|
|
|
|
class TestGetUserPreferences:
|
|
"""Tests for _get_user_preferences helper."""
|
|
|
|
def test_returns_empty_without_manager(self):
|
|
r = TopicBasedRecommender()
|
|
prefs = r._get_user_preferences("user1")
|
|
assert prefs == {}
|
|
|
|
def test_delegates_to_manager(self):
|
|
pm = Mock()
|
|
pm.get_preferences.return_value = {"key": "val"}
|
|
r = TopicBasedRecommender(preference_manager=pm)
|
|
prefs = r._get_user_preferences("user1")
|
|
assert prefs == {"key": "val"}
|
|
pm.get_preferences.assert_called_once_with("user1")
|
|
|
|
|
|
# --- Get user ratings ---
|
|
|
|
|
|
class TestGetUserRatings:
|
|
"""Tests for _get_user_ratings helper."""
|
|
|
|
def test_returns_empty_without_system(self):
|
|
r = TopicBasedRecommender()
|
|
ratings = r._get_user_ratings("user1")
|
|
assert ratings == []
|
|
|
|
def test_delegates_to_rating_system(self):
|
|
rs = Mock()
|
|
rs.get_recent_ratings.return_value = [{"id": 1}]
|
|
r = TopicBasedRecommender(rating_system=rs)
|
|
ratings = r._get_user_ratings("user1")
|
|
assert ratings == [{"id": 1}]
|
|
|
|
|
|
# --- Execute search ---
|
|
|
|
|
|
class TestExecuteSearch:
|
|
"""Tests for _execute_search helper."""
|
|
|
|
def test_returns_error_without_system(self):
|
|
r = TopicBasedRecommender()
|
|
result = r._execute_search("query")
|
|
assert "error" in result
|
|
|
|
def test_delegates_to_search_system(self):
|
|
ss = Mock()
|
|
ss.analyze_topic.return_value = {"results": []}
|
|
r = TopicBasedRecommender(search_system=ss)
|
|
result = r._execute_search("query")
|
|
assert result == {"results": []}
|
|
|
|
def test_handles_search_exception(self):
|
|
ss = Mock()
|
|
ss.analyze_topic.side_effect = Exception("search failed")
|
|
r = TopicBasedRecommender(search_system=ss)
|
|
result = r._execute_search("query")
|
|
assert "error" in result
|
|
|
|
|
|
# --- Strategy info ---
|
|
|
|
|
|
class TestStrategyInfo:
|
|
"""Tests for get_strategy_info."""
|
|
|
|
def test_has_name(self):
|
|
r = TopicBasedRecommender()
|
|
info = r.get_strategy_info()
|
|
assert info["name"] == "TopicBasedRecommender"
|
|
|
|
def test_reports_no_preference_manager(self):
|
|
r = TopicBasedRecommender()
|
|
info = r.get_strategy_info()
|
|
assert info["has_preference_manager"] is False
|
|
|
|
def test_reports_preference_manager_present(self):
|
|
r = TopicBasedRecommender(preference_manager=Mock())
|
|
info = r.get_strategy_info()
|
|
assert info["has_preference_manager"] is True
|
|
|
|
def test_reports_no_search_system(self):
|
|
r = TopicBasedRecommender()
|
|
info = r.get_strategy_info()
|
|
assert info["has_search_system"] is False
|
|
|
|
def test_has_description(self):
|
|
r = TopicBasedRecommender()
|
|
info = r.get_strategy_info()
|
|
assert "description" in info
|