""" Unit tests for app.services.news.session_window. Critical invariant: `post` must end at the next trading day's premarket start (04:00 ET) — NOT next session's open (09:30 ET) — to avoid 4-hour overlap with the next session's `premarket` window. """ from datetime import date, datetime, time from zoneinfo import ZoneInfo import pytest from app.services.news.session_window import session_window ET = ZoneInfo("America/New_York") UTC = ZoneInfo("UTC") def _et(d: date, t: time) -> datetime: return datetime.combine(d, t, tzinfo=ET).astimezone(UTC) def test_premarket_bounds_basic_weekday(): # Wednesday 2026-04-22 (Tue 2026-04-21 prev day, both weekdays) target = date(2026, 4, 22) start, end = session_window(target, "premarket") # Premarket starts at the previous session's *close*, not 04:00 expected_start = _et(date(2026, 4, 21), time(16, 0)) expected_end = _et(target, time(9, 30)) assert start == expected_start assert end == expected_end def test_intraday_bounds_basic_weekday(): target = date(2026, 4, 22) start, end = session_window(target, "intraday") assert start == _et(target, time(9, 30)) assert end == _et(target, time(16, 0)) def test_post_ends_at_next_premarket_start_not_next_open(): """post must NOT extend to next session's 09:30 — that would overlap T+1 premarket.""" target = date(2026, 4, 22) # Wed; next trading day Thu 2026-04-23 _, end = session_window(target, "post") expected_end = _et(date(2026, 4, 23), time(4, 0)) assert end == expected_end, f"post should end at next premarket start (04:00 ET), got {end.astimezone(ET)}" def test_post_and_next_premarket_are_disjoint(): target = date(2026, 4, 22) _, post_end = session_window(target, "post") next_pm_start, _ = session_window(date(2026, 4, 23), "premarket") # premarket starts at *previous close* (16:00), so they don't share boundary; # the contract is post_end <= 04:00 ET while next premarket fully overlaps that # zone. We assert the no-double-count guarantee: a 04:30 ET headline lands in # premarket (Thu) only, not in post (Wed). sample_dt = _et(date(2026, 4, 23), time(4, 30)) assert sample_dt >= post_end, "04:30 ET headline should be past post-window end" assert sample_dt > next_pm_start # premarket window includes 04:30 def test_full_session_spans_prev_close_to_next_premarket_start(): target = date(2026, 4, 22) start, end = session_window(target, "full_session") assert start == _et(date(2026, 4, 21), time(16, 0)) assert end == _et(date(2026, 4, 23), time(4, 0)) def test_premarket_after_weekend_uses_friday_close(): """Monday's premarket starts at the previous *Friday*'s close, not Sunday.""" target = date(2026, 4, 27) # Monday start, _ = session_window(target, "premarket") expected_start = _et(date(2026, 4, 24), time(16, 0)) # Friday assert start == expected_start def test_friday_post_extends_to_monday_premarket_start(): """Friday's `post` window must extend across the weekend to Monday 04:00 ET.""" target = date(2026, 4, 24) # Friday _, end = session_window(target, "post") expected_end = _et(date(2026, 4, 27), time(4, 0)) # Monday 04:00 assert end == expected_end def test_unknown_window_raises(): with pytest.raises(ValueError): session_window(date(2026, 4, 22), "lunch") # type: ignore[arg-type] def test_returned_datetimes_are_utc_aware(): start, end = session_window(date(2026, 4, 22), "premarket") assert start.tzinfo is not None assert end.tzinfo is not None assert start.utcoffset().total_seconds() == 0 # type: ignore[union-attr] assert end.utcoffset().total_seconds() == 0 # type: ignore[union-attr]