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

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