feat: extend RSS item retention from 14 to 90 days
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
CONTENT_MAX_CHARS = 2000
|
||||
# Keep only items from the last N days to avoid unbounded growth
|
||||
ITEM_MAX_AGE_DAYS = 14
|
||||
ITEM_MAX_AGE_DAYS = 90
|
||||
|
||||
|
||||
def extract_item(entry) -> dict:
|
||||
@@ -135,7 +135,7 @@ async def _prune_old_items(feed_id: int) -> None:
|
||||
text("""
|
||||
DELETE FROM rss_items
|
||||
WHERE feed_id = :feed_id
|
||||
AND published_at < NOW() - INTERVAL '14 days'
|
||||
AND published_at < NOW() - INTERVAL '90 days'
|
||||
""").bindparams(feed_id=feed_id)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for news API — retention constant and endpoint formatting."""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def test_rss_item_max_age_is_90_days():
|
||||
"""Retention window should be 90 days."""
|
||||
from fabledassistant.services.rss import ITEM_MAX_AGE_DAYS
|
||||
|
||||
assert ITEM_MAX_AGE_DAYS == 90
|
||||
|
||||
|
||||
def _make_mock_session(rows):
|
||||
"""Return a mock async context manager whose session.execute returns rows."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.mappings.return_value.all.return_value = rows
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_cm.__aexit__ = AsyncMock(return_value=False)
|
||||
return mock_cm
|
||||
|
||||
|
||||
def test_list_news_item_serialisation():
|
||||
"""News item serialisation should truncate content to 300 chars and include reaction."""
|
||||
pub = datetime(2026, 3, 28, 8, 0, tzinfo=timezone.utc)
|
||||
item = {
|
||||
"id": 1,
|
||||
"title": "EU AI Act deadline",
|
||||
"url": "https://example.com/ai",
|
||||
"content": "x" * 500,
|
||||
"published_at": pub,
|
||||
"topics": ["tech"],
|
||||
"feed_title": "TechCrunch",
|
||||
"reaction": "up",
|
||||
}
|
||||
|
||||
result = {
|
||||
"id": item["id"],
|
||||
"title": item["title"],
|
||||
"url": item["url"],
|
||||
"snippet": (item["content"] or "")[:300],
|
||||
"published_at": item["published_at"].isoformat() if item["published_at"] else None,
|
||||
"topics": item["topics"] or [],
|
||||
"source": item["feed_title"],
|
||||
"reaction": item["reaction"],
|
||||
}
|
||||
|
||||
assert result["snippet"] == "x" * 300
|
||||
assert result["source"] == "TechCrunch"
|
||||
assert result["reaction"] == "up"
|
||||
assert result["published_at"] == pub.isoformat()
|
||||
|
||||
|
||||
def test_list_news_null_content_serialisation():
|
||||
"""Null content should produce empty snippet without error."""
|
||||
item = {
|
||||
"id": 2,
|
||||
"title": "No content article",
|
||||
"url": "https://example.com/b",
|
||||
"content": None,
|
||||
"published_at": None,
|
||||
"topics": None,
|
||||
"feed_title": "Hacker News",
|
||||
"reaction": None,
|
||||
}
|
||||
|
||||
result = {
|
||||
"id": item["id"],
|
||||
"title": item["title"],
|
||||
"url": item["url"],
|
||||
"snippet": (item["content"] or "")[:300],
|
||||
"published_at": item["published_at"].isoformat() if item["published_at"] else None,
|
||||
"topics": item["topics"] or [],
|
||||
"source": item["feed_title"],
|
||||
"reaction": item["reaction"],
|
||||
}
|
||||
|
||||
assert result["snippet"] == ""
|
||||
assert result["topics"] == []
|
||||
assert result["published_at"] is None
|
||||
assert result["reaction"] is None
|
||||
Reference in New Issue
Block a user