Files
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

607 lines
20 KiB
Python

"""
Tests for base card classes in news/core/base_card.py.
Tests cover:
- CardSource dataclass
- CardVersion dataclass
- BaseCard abstract class methods
- NewsCard concrete implementation
- ResearchCard concrete implementation
- UpdateCard concrete implementation
- OverviewCard concrete implementation
"""
import pytest
from unittest.mock import patch, MagicMock
from datetime import datetime
from local_deep_research.news.core.base_card import (
CardSource,
CardVersion,
NewsCard,
ResearchCard,
UpdateCard,
OverviewCard,
)
@pytest.fixture
def fixed_time():
"""Return a fixed datetime for testing."""
return datetime(2024, 1, 15, 12, 0, 0)
@pytest.fixture
def mock_utc_now(fixed_time):
"""Mock utc_now to return consistent timestamps."""
with patch("local_deep_research.news.core.base_card.utc_now") as mock:
mock.return_value = fixed_time
yield fixed_time
@pytest.fixture
def mock_generate_card_id():
"""Mock card ID generation for consistent IDs."""
with patch(
"local_deep_research.news.core.base_card.generate_card_id"
) as mock:
mock.return_value = "test_card_id_123"
yield mock
class TestCardSource:
"""Tests for CardSource dataclass."""
def test_basic_creation(self):
"""Test basic CardSource creation."""
source = CardSource(type="news_item")
assert source.type == "news_item"
assert source.source_id is None
assert source.created_from == ""
assert source.metadata == {}
def test_full_creation(self):
"""Test CardSource with all fields."""
source = CardSource(
type="user_search",
source_id="search_123",
created_from="User search: AI",
metadata={"search_strategy": "quick"},
)
assert source.type == "user_search"
assert source.source_id == "search_123"
assert source.created_from == "User search: AI"
assert source.metadata == {"search_strategy": "quick"}
def test_metadata_default_is_independent(self):
"""Test metadata default doesn't share between instances."""
source1 = CardSource(type="test")
source2 = CardSource(type="test")
source1.metadata["key"] = "value"
assert "key" not in source2.metadata
class TestCardVersion:
"""Tests for CardVersion dataclass."""
def test_basic_creation(self, fixed_time):
"""Test basic CardVersion creation."""
version = CardVersion(
version_id="v1",
created_at=fixed_time,
content={"data": "test"},
query_used="test query",
)
assert version.version_id == "v1"
assert version.created_at == fixed_time
assert version.content == {"data": "test"}
assert version.query_used == "test query"
assert version.search_strategy is None
def test_with_search_strategy(self, fixed_time):
"""Test CardVersion with search strategy."""
version = CardVersion(
version_id="v1",
created_at=fixed_time,
content={},
query_used="query",
search_strategy="detailed",
)
assert version.search_strategy == "detailed"
def test_auto_generates_id_when_empty(
self, fixed_time, mock_generate_card_id
):
"""Test version_id is auto-generated when empty."""
version = CardVersion(
version_id="",
created_at=fixed_time,
content={},
query_used="query",
)
assert version.version_id == "test_card_id_123"
class TestNewsCard:
"""Tests for NewsCard concrete implementation."""
def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
"""Test basic NewsCard creation."""
source = CardSource(type="news_item")
card = NewsCard(
topic="AI Development",
source=source,
user_id="user_123",
)
assert card.topic == "AI Development"
assert card.user_id == "user_123"
assert card.id == "test_card_id_123"
assert card.headline == "AI Development" # Defaults to topic
def test_with_custom_headline(self, mock_utc_now, mock_generate_card_id):
"""Test NewsCard with custom headline."""
source = CardSource(type="news_item")
card = NewsCard(
topic="AI",
source=source,
user_id="user_123",
headline="Breaking: AI Achieves New Milestone",
)
assert card.headline == "Breaking: AI Achieves New Milestone"
def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
"""Test get_card_type returns 'news'."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
assert card.get_card_type() == "news"
def test_to_dict_includes_news_fields(
self, mock_utc_now, mock_generate_card_id
):
"""Test to_dict includes news-specific fields."""
source = CardSource(type="news_item")
card = NewsCard(
topic="AI",
source=source,
user_id="user_123",
headline="Test Headline",
summary="Test summary",
category="Technology",
impact_score=8,
)
result = card.to_dict()
assert result["headline"] == "Test Headline"
assert result["summary"] == "Test summary"
assert result["category"] == "Technology"
assert result["impact_score"] == 8
assert result["card_type"] == "news"
def test_default_values(self, mock_utc_now, mock_generate_card_id):
"""Test NewsCard default values."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
assert card.category == "General"
assert card.impact_score == 5
assert card.is_developing is False
assert card.time_ago == "recent"
assert card.entities == {
"people": [],
"places": [],
"organizations": [],
}
def test_interaction_initialized(self, mock_utc_now, mock_generate_card_id):
"""Test interaction tracking is initialized."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
assert card.interaction == {
"votes_up": 0,
"votes_down": 0,
"views": 0,
"shares": 0,
}
class TestResearchCard:
"""Tests for ResearchCard concrete implementation."""
def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
"""Test basic ResearchCard creation."""
source = CardSource(type="user_search")
card = ResearchCard(
topic="Machine Learning",
source=source,
user_id="user_123",
)
assert card.topic == "Machine Learning"
assert card.research_depth == "quick"
assert card.key_findings == []
assert card.sources_count == 0
def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
"""Test get_card_type returns 'research'."""
source = CardSource(type="user_search")
card = ResearchCard(topic="test", source=source, user_id="user_123")
assert card.get_card_type() == "research"
def test_to_dict_includes_research_fields(
self, mock_utc_now, mock_generate_card_id
):
"""Test to_dict includes research-specific fields."""
source = CardSource(type="user_search")
card = ResearchCard(
topic="test",
source=source,
user_id="user_123",
research_depth="detailed",
key_findings=["finding1", "finding2"],
sources_count=10,
)
result = card.to_dict()
assert result["research_depth"] == "detailed"
assert result["key_findings"] == ["finding1", "finding2"]
assert result["sources_count"] == 10
assert result["card_type"] == "research"
class TestUpdateCard:
"""Tests for UpdateCard concrete implementation."""
def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
"""Test basic UpdateCard creation."""
source = CardSource(type="subscription")
card = UpdateCard(
topic="Updates",
source=source,
user_id="user_123",
)
assert card.topic == "Updates"
assert card.update_type == "new_stories"
assert card.count == 0
assert card.preview_items == []
def test_since_timestamp_set(self, mock_utc_now, mock_generate_card_id):
"""Test since timestamp is set on creation."""
source = CardSource(type="subscription")
card = UpdateCard(topic="test", source=source, user_id="user_123")
assert card.since == mock_utc_now
def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
"""Test get_card_type returns 'update'."""
source = CardSource(type="subscription")
card = UpdateCard(topic="test", source=source, user_id="user_123")
assert card.get_card_type() == "update"
def test_to_dict_includes_update_fields(
self, mock_utc_now, mock_generate_card_id
):
"""Test to_dict includes update-specific fields."""
source = CardSource(type="subscription")
card = UpdateCard(
topic="test",
source=source,
user_id="user_123",
update_type="breaking",
count=5,
preview_items=[{"title": "Item 1"}],
)
result = card.to_dict()
assert result["update_type"] == "breaking"
assert result["count"] == 5
assert result["preview_items"] == [{"title": "Item 1"}]
assert "since" in result
assert result["card_type"] == "update"
class TestOverviewCard:
"""Tests for OverviewCard concrete implementation."""
def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
"""Test basic OverviewCard creation."""
source = CardSource(type="system")
card = OverviewCard(
source=source,
user_id="user_123",
)
# Topic is set automatically for overview cards
assert card.topic == "News Overview"
def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
"""Test get_card_type returns 'overview'."""
source = CardSource(type="system")
card = OverviewCard(source=source, user_id="user_123")
assert card.get_card_type() == "overview"
def test_default_stats(self, mock_utc_now, mock_generate_card_id):
"""Test default stats structure."""
source = CardSource(type="system")
card = OverviewCard(source=source, user_id="user_123")
assert card.stats == {
"total_new": 0,
"breaking": 0,
"relevant": 0,
"categories": {},
}
def test_to_dict_includes_overview_fields(
self, mock_utc_now, mock_generate_card_id
):
"""Test to_dict includes overview-specific fields."""
source = CardSource(type="system")
card = OverviewCard(
source=source,
user_id="user_123",
summary="Daily news summary",
top_stories=[{"title": "Top Story"}],
trend_analysis="Trending: AI",
)
result = card.to_dict()
assert result["summary"] == "Daily news summary"
assert result["top_stories"] == [{"title": "Top Story"}]
assert result["trend_analysis"] == "Trending: AI"
assert result["card_type"] == "overview"
class TestBaseCardMethods:
"""Tests for BaseCard concrete methods (using NewsCard as implementation)."""
def test_set_progress_callback(self, mock_utc_now, mock_generate_card_id):
"""Test setting progress callback."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
callback = MagicMock()
card.set_progress_callback(callback)
assert card.progress_callback is callback
def test_update_progress_calls_callback(
self, mock_utc_now, mock_generate_card_id
):
"""Test _update_progress calls the callback."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
callback = MagicMock()
card.set_progress_callback(callback)
card._update_progress("Processing...", 50, {"step": "analysis"})
callback.assert_called_once_with(
"Processing...", 50, {"step": "analysis"}
)
def test_update_progress_without_callback(
self, mock_utc_now, mock_generate_card_id
):
"""Test _update_progress does nothing without callback."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
# Should not raise
card._update_progress("Message", 50)
def test_to_base_dict(self, mock_utc_now, mock_generate_card_id):
"""Test to_base_dict includes common fields."""
source = CardSource(
type="news_item",
source_id="src_123",
created_from="Test source",
)
card = NewsCard(
topic="Test Topic",
source=source,
user_id="user_123",
parent_card_id="parent_456",
)
result = card.to_base_dict()
assert result["id"] == "test_card_id_123"
assert result["topic"] == "Test Topic"
assert result["user_id"] == "user_123"
assert result["parent_card_id"] == "parent_456"
assert result["versions_count"] == 0
assert "source" in result
assert result["source"]["type"] == "news_item"
def test_get_latest_version_empty(
self, mock_utc_now, mock_generate_card_id
):
"""Test get_latest_version returns None when no versions."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
assert card.get_latest_version() is None
def test_get_latest_version_returns_most_recent(
self, mock_utc_now, mock_generate_card_id
):
"""Test get_latest_version returns most recent version."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
# Add versions manually
v1 = CardVersion(
version_id="v1",
created_at=datetime(2024, 1, 1),
content={},
query_used="q1",
)
v2 = CardVersion(
version_id="v2",
created_at=datetime(2024, 1, 15),
content={},
query_used="q2",
)
card.versions = [v1, v2]
latest = card.get_latest_version()
assert latest.version_id == "v2"
class TestBaseCardHelperMethods:
"""Tests for BaseCard helper methods."""
def test_extract_headline_from_headline(
self, mock_utc_now, mock_generate_card_id
):
"""Test _extract_headline extracts from headline field."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
result = {"headline": "Test Headline"}
assert card._extract_headline(result) == "Test Headline"
def test_extract_headline_from_title(
self, mock_utc_now, mock_generate_card_id
):
"""Test _extract_headline falls back to title."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
result = {"title": "Test Title"}
assert card._extract_headline(result) == "Test Title"
def test_extract_headline_from_query(
self, mock_utc_now, mock_generate_card_id
):
"""Test _extract_headline falls back to query (truncated)."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
result = {"query": "A" * 150}
headline = card._extract_headline(result)
assert len(headline) == 100
def test_extract_summary(self, mock_utc_now, mock_generate_card_id):
"""Test _extract_summary extracts from multiple fields."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
# Try summary field
assert card._extract_summary({"summary": "Test"}) == "Test"
# Try current_knowledge field
assert (
card._extract_summary({"current_knowledge": "Knowledge"})
== "Knowledge"
)
# Try formatted_findings (truncated)
long_findings = "A" * 600
assert (
len(card._extract_summary({"formatted_findings": long_findings}))
== 500
)
def test_calculate_impact(self, mock_utc_now, mock_generate_card_id):
"""Test _calculate_impact calculates score correctly."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
# Empty result
assert card._calculate_impact({}) == 5
# With findings and sources
result = {
"findings": ["f1", "f2", "f3", "f4", "f5", "f6"],
"sources": ["s1", "s2", "s3", "s4"],
}
score = card._calculate_impact(result)
assert 1 <= score <= 10
def test_extract_topics_from_topics_field(
self, mock_utc_now, mock_generate_card_id
):
"""Test _extract_topics uses topics field."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
result = {"topics": ["AI", "ML", "Tech"]}
assert card._extract_topics(result) == ["AI", "ML", "Tech"]
def test_extract_topics_from_query(
self, mock_utc_now, mock_generate_card_id
):
"""Test _extract_topics extracts from query when no topics."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
result = {"query": "artificial intelligence machine learning"}
topics = card._extract_topics(result)
# Should extract words with length > 4
assert "artificial" in topics
assert "intelligence" in topics
def test_extract_entities(self, mock_utc_now, mock_generate_card_id):
"""Test _extract_entities returns entities or default structure."""
source = CardSource(type="news_item")
card = NewsCard(topic="test", source=source, user_id="user_123")
# With entities
result = {"entities": {"people": ["John"], "places": ["NYC"]}}
assert card._extract_entities(result) == {
"people": ["John"],
"places": ["NYC"],
}
# Without entities
entities = card._extract_entities({})
assert entities == {"people": [], "places": [], "organizations": []}
class TestBaseCardWithCustomId:
"""Tests for BaseCard with custom ID."""
def test_uses_provided_card_id(self, mock_utc_now):
"""Test card uses provided card_id instead of generating."""
source = CardSource(type="news_item")
card = NewsCard(
topic="test",
source=source,
user_id="user_123",
card_id="custom_id_456",
)
assert card.id == "custom_id_456"
def test_generates_id_when_not_provided(
self, mock_utc_now, mock_generate_card_id
):
"""Test card generates ID when not provided."""
source = CardSource(type="news_item")
card = NewsCard(
topic="test",
source=source,
user_id="user_123",
)
assert card.id == "test_card_id_123"