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

722 lines
22 KiB
Python

"""
Deep behavioral tests for BaseCard and all card subclasses.
Tests real logic: initialization, helper methods, serialization, and edge cases.
"""
import uuid
from datetime import datetime, timezone
from unittest.mock import Mock
import pytest
from local_deep_research.news.core.base_card import (
CardSource,
CardVersion,
NewsCard,
OverviewCard,
ResearchCard,
UpdateCard,
)
# --- CardSource tests ---
class TestCardSourceDataclass:
"""Tests for CardSource dataclass behavior."""
def test_requires_type(self):
"""Test type is required."""
with pytest.raises(TypeError):
CardSource()
def test_type_stored(self):
source = CardSource(type="news_item")
assert source.type == "news_item"
def test_source_id_defaults_none(self):
source = CardSource(type="news_item")
assert source.source_id is None
def test_created_from_defaults_empty(self):
source = CardSource(type="news_item")
assert source.created_from == ""
def test_metadata_defaults_empty_dict(self):
source = CardSource(type="news_item")
assert source.metadata == {}
def test_metadata_independent_per_instance(self):
"""Test metadata dicts are independent between instances."""
s1 = CardSource(type="a")
s2 = CardSource(type="b")
s1.metadata["key"] = "value"
assert "key" not in s2.metadata
def test_all_fields(self):
source = CardSource(
type="user_search",
source_id="search-123",
created_from="manual",
metadata={"custom": True},
)
assert source.type == "user_search"
assert source.source_id == "search-123"
assert source.created_from == "manual"
assert source.metadata == {"custom": True}
# --- CardVersion tests ---
class TestCardVersionDataclass:
"""Tests for CardVersion dataclass behavior."""
def test_auto_generates_version_id_if_empty(self):
"""Test __post_init__ generates id when version_id is empty string."""
v = CardVersion(
version_id="",
created_at=datetime.now(timezone.utc),
content={"data": 1},
query_used="test",
)
assert v.version_id != ""
uuid.UUID(v.version_id) # Should be valid UUID
def test_keeps_provided_version_id(self):
v = CardVersion(
version_id="custom-id",
created_at=datetime.now(timezone.utc),
content={},
query_used="q",
)
assert v.version_id == "custom-id"
def test_stores_content(self):
content = {"findings": ["a", "b"]}
v = CardVersion(
version_id="v1",
created_at=datetime.now(timezone.utc),
content=content,
query_used="q",
)
assert v.content == content
def test_stores_query_used(self):
v = CardVersion(
version_id="v1",
created_at=datetime.now(timezone.utc),
content={},
query_used="artificial intelligence",
)
assert v.query_used == "artificial intelligence"
def test_search_strategy_defaults_none(self):
v = CardVersion(
version_id="v1",
created_at=datetime.now(timezone.utc),
content={},
query_used="q",
)
assert v.search_strategy is None
def test_search_strategy_stored(self):
v = CardVersion(
version_id="v1",
created_at=datetime.now(timezone.utc),
content={},
query_used="q",
search_strategy="news_aggregation",
)
assert v.search_strategy == "news_aggregation"
# --- NewsCard tests ---
class TestNewsCardInit:
"""Tests for NewsCard initialization and __post_init__."""
def _make_source(self):
return CardSource(type="news_item", source_id="src-1")
def test_id_is_valid_uuid(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
uuid.UUID(card.id)
def test_custom_card_id_used(self):
card = NewsCard(
topic="AI",
source=self._make_source(),
user_id="u1",
card_id="custom-id",
)
assert card.id == "custom-id"
def test_created_at_is_utc(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.created_at.tzinfo is not None
def test_updated_at_set(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.updated_at is not None
def test_interaction_initialized(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.interaction == {
"votes_up": 0,
"votes_down": 0,
"views": 0,
"shares": 0,
}
def test_headline_defaults_to_topic(self):
card = NewsCard(
topic="Big News", source=self._make_source(), user_id="u1"
)
assert card.headline == "Big News"
def test_headline_override(self):
card = NewsCard(
topic="Topic",
source=self._make_source(),
user_id="u1",
headline="Custom Headline",
)
assert card.headline == "Custom Headline"
def test_default_category(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.category == "General"
def test_default_impact_score(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.impact_score == 5
def test_default_entities(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.entities == {
"people": [],
"places": [],
"organizations": [],
}
def test_default_is_developing(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.is_developing is False
def test_versions_empty_by_default(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.versions == []
def test_get_card_type(self):
card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
assert card.get_card_type() == "news"
class TestNewsCardToDict:
"""Tests for NewsCard.to_dict()."""
def _make_card(self, **kwargs):
source = CardSource(type="news_item", source_id="src-1")
defaults = {
"topic": "Test",
"source": source,
"user_id": "u1",
}
defaults.update(kwargs)
return NewsCard(**defaults)
def test_includes_id(self):
card = self._make_card()
d = card.to_dict()
assert "id" in d
def test_includes_topic(self):
card = self._make_card(topic="Science")
d = card.to_dict()
assert d["topic"] == "Science"
def test_includes_card_type(self):
card = self._make_card()
d = card.to_dict()
assert d["card_type"] == "news"
def test_includes_headline(self):
card = self._make_card(headline="Breaking!")
d = card.to_dict()
assert d["headline"] == "Breaking!"
def test_includes_category(self):
card = self._make_card(category="Tech")
d = card.to_dict()
assert d["category"] == "Tech"
def test_includes_impact_score(self):
card = self._make_card(impact_score=8)
d = card.to_dict()
assert d["impact_score"] == 8
def test_includes_is_developing(self):
card = self._make_card(is_developing=True)
d = card.to_dict()
assert d["is_developing"] is True
def test_includes_source_info(self):
card = self._make_card()
d = card.to_dict()
assert d["source"]["type"] == "news_item"
assert d["source"]["source_id"] == "src-1"
def test_versions_count(self):
card = self._make_card()
d = card.to_dict()
assert d["versions_count"] == 0
def test_includes_user_id(self):
card = self._make_card(user_id="user42")
d = card.to_dict()
assert d["user_id"] == "user42"
def test_includes_interaction(self):
card = self._make_card()
d = card.to_dict()
assert "interaction" in d
assert d["interaction"]["votes_up"] == 0
def test_created_at_is_isoformat(self):
card = self._make_card()
d = card.to_dict()
# Should be parseable ISO format
datetime.fromisoformat(d["created_at"])
# --- BaseCard helper methods ---
class TestExtractHeadline:
"""Tests for BaseCard._extract_headline()."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_uses_headline_field(self):
card = self._make_card()
result = card._extract_headline({"headline": "Big Story"})
assert result == "Big Story"
def test_falls_back_to_title(self):
card = self._make_card()
result = card._extract_headline({"title": "A Title"})
assert result == "A Title"
def test_falls_back_to_query_truncated(self):
card = self._make_card()
long_query = "x" * 200
result = card._extract_headline({"query": long_query})
assert len(result) <= 100
def test_returns_empty_if_nothing(self):
card = self._make_card()
result = card._extract_headline({})
assert result == ""
def test_headline_takes_priority_over_title(self):
card = self._make_card()
result = card._extract_headline({"headline": "H", "title": "T"})
assert result == "H"
class TestExtractSummary:
"""Tests for BaseCard._extract_summary()."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_uses_summary_field(self):
card = self._make_card()
result = card._extract_summary({"summary": "A summary"})
assert result == "A summary"
def test_falls_back_to_current_knowledge(self):
card = self._make_card()
result = card._extract_summary({"current_knowledge": "Knowledge"})
assert result == "Knowledge"
def test_falls_back_to_formatted_findings_truncated(self):
card = self._make_card()
long_findings = "y" * 1000
result = card._extract_summary({"formatted_findings": long_findings})
assert len(result) <= 500
def test_returns_empty_if_nothing(self):
card = self._make_card()
result = card._extract_summary({})
assert result == ""
class TestCalculateImpact:
"""Tests for BaseCard._calculate_impact() scoring logic."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_base_score_is_5_with_no_findings(self):
card = self._make_card()
score = card._calculate_impact({})
assert score == 5
def test_score_increases_with_findings(self):
card = self._make_card()
findings = [f"finding-{i}" for i in range(10)]
score = card._calculate_impact({"findings": findings})
assert score > 5
def test_score_increases_with_sources(self):
card = self._make_card()
sources = [f"source-{i}" for i in range(9)]
score = card._calculate_impact({"sources": sources})
assert score > 5
def test_score_capped_at_10(self):
card = self._make_card()
result = {"findings": list(range(100)), "sources": list(range(100))}
score = card._calculate_impact(result)
assert score == 10
def test_score_minimum_is_1(self):
card = self._make_card()
score = card._calculate_impact({"findings": [], "sources": []})
assert score >= 1
def test_score_formula_5_findings_adds_1(self):
card = self._make_card()
# 5 findings = 5 + 1 = 6
score = card._calculate_impact({"findings": [1, 2, 3, 4, 5]})
assert score == 6
def test_score_formula_3_sources_adds_1(self):
card = self._make_card()
# 3 sources = 5 + 1 = 6
score = card._calculate_impact({"sources": [1, 2, 3]})
assert score == 6
def test_combined_scoring(self):
card = self._make_card()
# 10 findings + 6 sources = 5 + 2 + 2 = 9
score = card._calculate_impact(
{"findings": list(range(10)), "sources": list(range(6))}
)
assert score == 9
class TestExtractTopics:
"""Tests for BaseCard._extract_topics()."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_uses_topics_field(self):
card = self._make_card()
result = card._extract_topics({"topics": ["AI", "ML"]})
assert result == ["AI", "ML"]
def test_extracts_from_query_when_no_topics(self):
card = self._make_card()
result = card._extract_topics(
{"query": "artificial intelligence breakthroughs"}
)
# Only words > 4 chars
assert "artificial" in result
assert "intelligence" in result
assert "breakthroughs" in result
def test_limits_extracted_topics_to_5(self):
card = self._make_card()
long_query = " ".join([f"longword{i}" for i in range(20)])
result = card._extract_topics({"query": long_query})
assert len(result) <= 5
def test_filters_short_words_from_query(self):
card = self._make_card()
result = card._extract_topics({"query": "the big new ai event today"})
assert "the" not in result
assert "big" not in result
assert "new" not in result
assert "ai" not in result
def test_returns_empty_when_no_data(self):
card = self._make_card()
result = card._extract_topics({})
assert result == []
class TestExtractEntities:
"""Tests for BaseCard._extract_entities()."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_uses_entities_field(self):
card = self._make_card()
entities = {"people": ["Alice"], "places": [], "organizations": []}
result = card._extract_entities({"entities": entities})
assert result == entities
def test_default_empty_entities(self):
card = self._make_card()
result = card._extract_entities({})
assert result == {"people": [], "places": [], "organizations": []}
class TestGetLatestVersion:
"""Tests for BaseCard.get_latest_version()."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_returns_none_when_no_versions(self):
card = self._make_card()
assert card.get_latest_version() is None
def test_returns_single_version(self):
card = self._make_card()
v = CardVersion(
version_id="v1",
created_at=datetime.now(timezone.utc),
content={},
query_used="q",
)
card.versions.append(v)
assert card.get_latest_version() is v
def test_returns_latest_by_created_at(self):
card = self._make_card()
v1 = CardVersion(
version_id="v1",
created_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
content={},
query_used="q",
)
v2 = CardVersion(
version_id="v2",
created_at=datetime(2024, 6, 1, tzinfo=timezone.utc),
content={},
query_used="q",
)
v3 = CardVersion(
version_id="v3",
created_at=datetime(2024, 3, 1, tzinfo=timezone.utc),
content={},
query_used="q",
)
card.versions.extend([v1, v2, v3])
assert card.get_latest_version() is v2
class TestProgressCallback:
"""Tests for progress callback mechanism."""
def _make_card(self):
source = CardSource(type="news_item")
return NewsCard(topic="Test", source=source, user_id="u1")
def test_callback_not_called_when_not_set(self):
card = self._make_card()
# Should not raise
card._update_progress("msg")
def test_callback_called_when_set(self):
card = self._make_card()
cb = Mock()
card.set_progress_callback(cb)
card._update_progress("searching", 50, {"step": 1})
cb.assert_called_once_with("searching", 50, {"step": 1})
def test_callback_default_metadata(self):
card = self._make_card()
cb = Mock()
card.set_progress_callback(cb)
card._update_progress("msg", 10)
cb.assert_called_once_with("msg", 10, {})
# --- ResearchCard tests ---
class TestResearchCard:
"""Tests for ResearchCard-specific behavior."""
def _make_source(self):
return CardSource(type="research")
def test_get_card_type(self):
card = ResearchCard(
topic="AI", source=self._make_source(), user_id="u1"
)
assert card.get_card_type() == "research"
def test_default_depth(self):
card = ResearchCard(
topic="AI", source=self._make_source(), user_id="u1"
)
assert card.research_depth == "quick"
def test_default_findings(self):
card = ResearchCard(
topic="AI", source=self._make_source(), user_id="u1"
)
assert card.key_findings == []
def test_default_sources_count(self):
card = ResearchCard(
topic="AI", source=self._make_source(), user_id="u1"
)
assert card.sources_count == 0
def test_to_dict_includes_depth(self):
card = ResearchCard(
topic="AI",
source=self._make_source(),
user_id="u1",
research_depth="detailed",
)
d = card.to_dict()
assert d["research_depth"] == "detailed"
def test_to_dict_includes_findings(self):
card = ResearchCard(
topic="AI",
source=self._make_source(),
user_id="u1",
key_findings=["finding1", "finding2"],
)
d = card.to_dict()
assert d["key_findings"] == ["finding1", "finding2"]
def test_to_dict_includes_sources_count(self):
card = ResearchCard(
topic="AI",
source=self._make_source(),
user_id="u1",
sources_count=15,
)
d = card.to_dict()
assert d["sources_count"] == 15
# --- UpdateCard tests ---
class TestUpdateCard:
"""Tests for UpdateCard-specific behavior."""
def _make_source(self):
return CardSource(type="update")
def test_get_card_type(self):
card = UpdateCard(
topic="Updates", source=self._make_source(), user_id="u1"
)
assert card.get_card_type() == "update"
def test_default_update_type(self):
card = UpdateCard(
topic="Updates", source=self._make_source(), user_id="u1"
)
assert card.update_type == "new_stories"
def test_since_is_set(self):
card = UpdateCard(
topic="Updates", source=self._make_source(), user_id="u1"
)
assert card.since is not None
assert card.since.tzinfo is not None
def test_to_dict_includes_update_type(self):
card = UpdateCard(
topic="Updates",
source=self._make_source(),
user_id="u1",
update_type="breaking",
)
d = card.to_dict()
assert d["update_type"] == "breaking"
def test_to_dict_includes_count(self):
card = UpdateCard(
topic="Updates",
source=self._make_source(),
user_id="u1",
count=42,
)
d = card.to_dict()
assert d["count"] == 42
def test_to_dict_since_is_isoformat(self):
card = UpdateCard(
topic="Updates", source=self._make_source(), user_id="u1"
)
d = card.to_dict()
datetime.fromisoformat(d["since"])
# --- OverviewCard tests ---
class TestOverviewCard:
"""Tests for OverviewCard-specific behavior."""
def _make_source(self):
return CardSource(type="overview")
def test_get_card_type(self):
card = OverviewCard(source=self._make_source(), user_id="u1")
assert card.get_card_type() == "overview"
def test_topic_defaults_to_news_overview(self):
card = OverviewCard(source=self._make_source(), user_id="u1")
assert card.topic == "News Overview"
def test_default_stats(self):
card = OverviewCard(source=self._make_source(), user_id="u1")
assert card.stats == {
"total_new": 0,
"breaking": 0,
"relevant": 0,
"categories": {},
}
def test_default_summary(self):
card = OverviewCard(source=self._make_source(), user_id="u1")
assert card.summary == ""
def test_to_dict_includes_stats(self):
card = OverviewCard(
source=self._make_source(),
user_id="u1",
stats={
"total_new": 5,
"breaking": 1,
"relevant": 3,
"categories": {},
},
)
d = card.to_dict()
assert d["stats"]["total_new"] == 5
def test_to_dict_includes_trend_analysis(self):
card = OverviewCard(
source=self._make_source(),
user_id="u1",
trend_analysis="AI is trending",
)
d = card.to_dict()
assert d["trend_analysis"] == "AI is trending"