7602bf2293
Backend: - Delete briefing services (pipeline, scheduler, conversations, profile, tools) - Delete routes/briefing.py + remove blueprint registration - Move _get_temp_unit into services/weather.get_temp_unit (reads top-level temp_unit setting) - Rename briefing_preferences.py → rss_filtering.py (functions are RSS-specific) - Strip briefing scheduler hooks from app.py - Strip briefing scheduler call from routes/settings.py - Update test imports (test_rss_service, test_tz_helpers) Frontend: - Delete BriefingView, BriefingSetupWizard, BriefingToolStatusRow - Strip /briefing route + nav links (AppHeader, KnowledgeView) - Strip Settings → Briefing tab + state + functions + imports - Strip briefing-intermediate handling from ChatMessage - Hide /news route + nav links (NewsView depended on briefing endpoints; orphaned in tree) - Drop unused useSettingsStore from AppHeader The Android BriefingScreen lives in a separate repo and is not touched here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
133 lines
4.9 KiB
Python
133 lines
4.9 KiB
Python
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
def test_extract_item_fields():
|
|
"""extract_item() should pull title, link, id, summary from a feedparser entry."""
|
|
from fabledassistant.services.rss import extract_item
|
|
entry = MagicMock()
|
|
entry.get = lambda k, d="": {"title": "Test Post", "link": "https://example.com/1",
|
|
"id": "guid-1", "summary": "A summary"}.get(k, d)
|
|
entry.published_parsed = None
|
|
item = extract_item(entry)
|
|
assert item["title"] == "Test Post"
|
|
assert item["url"] == "https://example.com/1"
|
|
assert item["guid"] == "guid-1"
|
|
assert item["content"] == "A summary"
|
|
|
|
|
|
def test_extract_item_does_not_truncate_content():
|
|
"""extract_item() should store content without truncation."""
|
|
from fabledassistant.services.rss import extract_item
|
|
long_text = "x" * 100_000
|
|
entry = MagicMock()
|
|
entry.get = lambda k, d="": {"summary": long_text, "title": "", "link": "", "id": "g"}.get(k, d)
|
|
entry.content = []
|
|
entry.published_parsed = None
|
|
item = extract_item(entry)
|
|
assert len(item["content"]) == 100_000
|
|
|
|
|
|
def test_extract_item_prefers_content_over_summary():
|
|
"""extract_item() should prefer 'content' field over 'summary' when present."""
|
|
from fabledassistant.services.rss import extract_item
|
|
entry = MagicMock()
|
|
content_obj = MagicMock()
|
|
content_obj.value = "Full content here"
|
|
entry.get = lambda k, d="": {
|
|
"title": "T", "link": "http://x.com", "id": "g",
|
|
"summary": "Short summary",
|
|
}.get(k, d)
|
|
entry.content = [content_obj]
|
|
entry.published_parsed = None
|
|
item = extract_item(entry)
|
|
assert item["content"] == "Full content here"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_classify_items_batch_returns_topic_map():
|
|
"""classify_items_batch should return a dict mapping item_id to topic list."""
|
|
from unittest.mock import AsyncMock
|
|
from fabledassistant.services.rss_classifier import classify_items_batch
|
|
|
|
fake_response = '{"1": ["technology", "ai"], "2": ["politics"]}'
|
|
|
|
with patch(
|
|
"fabledassistant.services.rss_classifier._llm_classify",
|
|
new_callable=AsyncMock,
|
|
return_value=fake_response,
|
|
):
|
|
items = [
|
|
{"id": 1, "title": "OpenAI releases GPT-5", "content": "..."},
|
|
{"id": 2, "title": "EU passes new law", "content": "..."},
|
|
]
|
|
result = await classify_items_batch(items, user_include_topics=[])
|
|
assert result[1] == ["technology", "ai"]
|
|
assert result[2] == ["politics"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_classify_items_batch_handles_llm_failure():
|
|
"""classify_items_batch should return empty dict on LLM error."""
|
|
from unittest.mock import AsyncMock
|
|
from fabledassistant.services.rss_classifier import classify_items_batch
|
|
|
|
with patch(
|
|
"fabledassistant.services.rss_classifier._llm_classify",
|
|
new_callable=AsyncMock,
|
|
side_effect=Exception("LLM unavailable"),
|
|
):
|
|
items = [{"id": 5, "title": "Some news", "content": ""}]
|
|
result = await classify_items_batch(items, user_include_topics=[])
|
|
assert result == {}
|
|
|
|
|
|
def test_score_rss_items_excludes_blacklisted_topics():
|
|
"""Items with excluded topics should be removed."""
|
|
from fabledassistant.services.rss_filtering import score_and_filter_items
|
|
|
|
items = [
|
|
{"id": 1, "title": "Tech news", "topics": ["technology"], "published_at": "2026-03-25T08:00:00"},
|
|
{"id": 2, "title": "Sports score", "topics": ["sports"], "published_at": "2026-03-25T08:00:00"},
|
|
]
|
|
result = score_and_filter_items(
|
|
items,
|
|
include_topics=["technology"],
|
|
exclude_topics=["sports"],
|
|
topic_scores={},
|
|
max_items=10,
|
|
)
|
|
ids = [r["id"] for r in result]
|
|
assert 1 in ids
|
|
assert 2 not in ids
|
|
|
|
|
|
def test_score_rss_items_boosts_included_topics():
|
|
"""Items matching include_topics should rank higher than neutral items."""
|
|
from fabledassistant.services.rss_filtering import score_and_filter_items
|
|
|
|
items = [
|
|
{"id": 1, "title": "Random news", "topics": ["other"], "published_at": "2026-03-25T07:00:00"},
|
|
{"id": 2, "title": "Tech news", "topics": ["technology"], "published_at": "2026-03-25T06:00:00"},
|
|
]
|
|
result = score_and_filter_items(
|
|
items,
|
|
include_topics=["technology"],
|
|
exclude_topics=[],
|
|
topic_scores={},
|
|
max_items=10,
|
|
)
|
|
assert result[0]["id"] == 2
|
|
|
|
|
|
def test_score_rss_items_no_preferences_returns_all():
|
|
"""With no preferences, all items should be returned sorted by recency."""
|
|
from fabledassistant.services.rss_filtering import score_and_filter_items
|
|
|
|
items = [
|
|
{"id": 1, "title": "A", "topics": [], "published_at": "2026-03-24T10:00:00"},
|
|
{"id": 2, "title": "B", "topics": [], "published_at": "2026-03-25T10:00:00"},
|
|
]
|
|
result = score_and_filter_items(items, [], [], {}, max_items=10)
|
|
assert result[0]["id"] == 2 # Newer first
|