Files
learningcircuit--local-deep…/tests/news/test_format_time_ago_behavior.py
T
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

292 lines
9.1 KiB
Python

"""
Deep behavioral tests for _format_time_ago utility function.
Tests time formatting, edge cases, pluralization, timezone handling, and error recovery.
"""
from datetime import datetime, timedelta, timezone
import pytest
from local_deep_research.news.api import _format_time_ago
# --- Days ago ---
class TestFormatTimeAgoDays:
"""Tests for day-based formatting."""
def test_one_day_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
result = _format_time_ago(ts)
assert result == "1 day ago"
def test_two_days_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
result = _format_time_ago(ts)
assert result == "2 days ago"
def test_seven_days_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
result = _format_time_ago(ts)
assert result == "7 days ago"
def test_thirty_days_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
result = _format_time_ago(ts)
assert result == "30 days ago"
def test_one_day_no_plural(self):
ts = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
result = _format_time_ago(ts)
assert "days" not in result
assert "day" in result
def test_three_days_plural(self):
ts = (datetime.now(timezone.utc) - timedelta(days=3)).isoformat()
result = _format_time_ago(ts)
assert "days" in result
def test_365_days_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(days=365)).isoformat()
result = _format_time_ago(ts)
assert "365 days ago" == result
# --- Hours ago ---
class TestFormatTimeAgoHours:
"""Tests for hour-based formatting."""
def test_two_hours_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
result = _format_time_ago(ts)
assert result == "2 hours ago"
def test_one_hour_ago(self):
ts = (
datetime.now(timezone.utc) - timedelta(hours=1, minutes=30)
).isoformat()
result = _format_time_ago(ts)
assert result == "1 hour ago"
def test_one_hour_no_plural(self):
ts = (
datetime.now(timezone.utc) - timedelta(hours=1, minutes=30)
).isoformat()
result = _format_time_ago(ts)
assert "hours" not in result
assert "hour" in result
def test_twelve_hours_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(hours=12)).isoformat()
result = _format_time_ago(ts)
assert result == "12 hours ago"
def test_twenty_three_hours_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(hours=23)).isoformat()
result = _format_time_ago(ts)
assert result == "23 hours ago"
def test_six_hours_plural(self):
ts = (datetime.now(timezone.utc) - timedelta(hours=6)).isoformat()
result = _format_time_ago(ts)
assert "hours" in result
# --- Minutes ago ---
class TestFormatTimeAgoMinutes:
"""Tests for minute-based formatting."""
def test_five_minutes_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
result = _format_time_ago(ts)
assert result == "5 minutes ago"
def test_one_minute_ago(self):
ts = (
datetime.now(timezone.utc) - timedelta(minutes=1, seconds=30)
).isoformat()
result = _format_time_ago(ts)
assert result == "1 minute ago"
def test_one_minute_no_plural(self):
ts = (
datetime.now(timezone.utc) - timedelta(minutes=1, seconds=30)
).isoformat()
result = _format_time_ago(ts)
assert "minutes" not in result
assert "minute" in result
def test_thirty_minutes_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
result = _format_time_ago(ts)
assert result == "30 minutes ago"
def test_fifty_nine_minutes_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(minutes=59)).isoformat()
result = _format_time_ago(ts)
assert result == "59 minutes ago"
# --- Just now ---
class TestFormatTimeAgoJustNow:
"""Tests for 'Just now' output."""
def test_zero_seconds(self):
ts = datetime.now(timezone.utc).isoformat()
result = _format_time_ago(ts)
assert result == "Just now"
def test_thirty_seconds_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(seconds=30)).isoformat()
result = _format_time_ago(ts)
assert result == "Just now"
def test_fifty_nine_seconds_ago(self):
ts = (datetime.now(timezone.utc) - timedelta(seconds=59)).isoformat()
result = _format_time_ago(ts)
assert result == "Just now"
# --- Timezone handling ---
class TestFormatTimeAgoTimezone:
"""Tests for timezone handling."""
def test_utc_timestamp(self):
ts = (datetime.now(timezone.utc) - timedelta(hours=3)).isoformat()
result = _format_time_ago(ts)
assert result == "3 hours ago"
def test_z_suffix(self):
dt = datetime.now(timezone.utc) - timedelta(hours=2)
ts = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
result = _format_time_ago(ts)
assert "2 hours ago" == result
def test_plus_offset(self):
dt = datetime.now(timezone.utc) - timedelta(hours=5)
ts = dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
result = _format_time_ago(ts)
assert result == "5 hours ago"
def test_naive_datetime_assumed_utc(self):
"""Naive timestamps should be treated as UTC."""
dt = datetime.now(timezone.utc) - timedelta(hours=4)
# Strip timezone info to make it naive
ts = dt.strftime("%Y-%m-%dT%H:%M:%S")
result = _format_time_ago(ts)
assert result == "4 hours ago"
# --- Error handling ---
class TestFormatTimeAgoErrorHandling:
"""Unparseable input raises rather than returning a neutral label.
created_at is NOT NULL and always written as isoformat(), so anything
that won't parse is corrupt data. The per-row loop in get_news_feed
catches the raise, logs it, and skips the row.
"""
def test_invalid_string_raises(self):
with pytest.raises(ValueError):
_format_time_ago("not-a-timestamp")
def test_empty_string_raises(self):
with pytest.raises(ValueError):
_format_time_ago("")
def test_none_raises(self):
with pytest.raises(TypeError):
_format_time_ago(None)
def test_integer_raises(self):
with pytest.raises(TypeError):
_format_time_ago(12345)
def test_gibberish_raises(self):
with pytest.raises(ValueError):
_format_time_ago("xyz123!@#")
# --- Input format variants ---
class TestFormatTimeAgoInputFormats:
"""Tests for various timestamp format inputs."""
def test_iso_format(self):
ts = (
datetime.now(timezone.utc) - timedelta(hours=1, minutes=30)
).isoformat()
result = _format_time_ago(ts)
assert "hour" in result
def test_date_only_string(self):
# Yesterday's date
yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).strftime(
"%Y-%m-%d"
)
result = _format_time_ago(yesterday)
assert "day" in result
def test_datetime_object_as_string(self):
dt = datetime.now(timezone.utc) - timedelta(minutes=10)
result = _format_time_ago(str(dt))
assert "10 minutes ago" == result
def test_microseconds_in_timestamp(self):
ts = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
result = _format_time_ago(ts)
assert result == "2 hours ago"
# --- Boundary conditions ---
class TestFormatTimeAgoBoundaries:
"""Tests for boundary conditions between time ranges."""
def test_exactly_60_seconds(self):
"""60 seconds should show as 1 minute, not 'Just now'."""
ts = (datetime.now(timezone.utc) - timedelta(seconds=61)).isoformat()
result = _format_time_ago(ts)
assert "minute" in result
def test_exactly_3600_seconds(self):
"""3600 seconds should show as 1 hour, not minutes."""
ts = (datetime.now(timezone.utc) - timedelta(seconds=3601)).isoformat()
result = _format_time_ago(ts)
assert "hour" in result
def test_just_under_one_day(self):
"""23 hours 59 min should show hours, not days."""
ts = (
datetime.now(timezone.utc) - timedelta(hours=23, minutes=59)
).isoformat()
result = _format_time_ago(ts)
assert "hour" in result
def test_exactly_one_day(self):
"""24 hours should show as 1 day."""
ts = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
result = _format_time_ago(ts)
assert "1 day ago" == result
def test_just_over_one_hour(self):
"""1 hour 1 second should show 1 hour."""
ts = (
datetime.now(timezone.utc) - timedelta(hours=1, seconds=1)
).isoformat()
result = _format_time_ago(ts)
assert "1 hour ago" == result