"""Tests: AutoScheduler catchup re-runs missed phases after restart.""" from __future__ import annotations import datetime as dt from unittest.mock import AsyncMock, MagicMock, patch from zoneinfo import ZoneInfo import pytest ET = ZoneInfo("America/New_York") def _make_scheduler(): from apps.web.paper_trading_service import AutoScheduler sched = AutoScheduler() sched._sessions = ["session_a"] sched._db_path = "/tmp/test.db" sched._log = lambda msg: None sched._dry_run = False return sched @pytest.mark.asyncio async def test_run_open_reruns_if_missed_and_not_in_phases(): """If server restarts at 10 AM ET and run_open not in processed_phases, it must run.""" sched = _make_scheduler() # now_et = 10:00 AM ET on a trading day now_et = dt.datetime(2026, 4, 24, 10, 0, 0, tzinfo=ET) mock_state = MagicMock() mock_session = MagicMock() mock_session.session_id = "sid-abc" mock_state.get_session.return_value = mock_session mock_state.is_phase_processed.return_value = False # not yet run run_trading_calls = [] async def fake_run_trading(cmd, sessions): run_trading_calls.append(cmd) with ( patch.object(sched, "_now_et", return_value=now_et), patch.object(sched, "_is_trading_day", return_value=True), patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)), patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True), patch.object(sched, "_run_trading", side_effect=fake_run_trading), patch("apps.paper_trader.state.StateManager", return_value=mock_state), ): await sched._run_catchup() assert "run-open" in run_trading_calls @pytest.mark.asyncio async def test_run_open_not_rerun_if_already_in_phases(): """If run_open already succeeded today (in processed_phases), skip it.""" sched = _make_scheduler() now_et = dt.datetime(2026, 4, 24, 11, 0, 0, tzinfo=ET) mock_state = MagicMock() mock_session = MagicMock() mock_session.session_id = "sid-abc" mock_state.get_session.return_value = mock_session mock_state.is_phase_processed.return_value = True # already ran run_trading_calls = [] async def fake_run_trading(cmd, sessions): run_trading_calls.append(cmd) with ( patch.object(sched, "_now_et", return_value=now_et), patch.object(sched, "_is_trading_day", return_value=True), patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)), patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True), patch.object(sched, "_run_trading", side_effect=fake_run_trading), patch("apps.paper_trader.state.StateManager", return_value=mock_state), ): await sched._run_catchup() assert "run-open" not in run_trading_calls @pytest.mark.asyncio async def test_run_close_reruns_if_missed(): """If server restarts at 16:00 ET and run_close not in processed_phases, it must run.""" sched = _make_scheduler() now_et = dt.datetime(2026, 4, 24, 16, 0, 0, tzinfo=ET) mock_state = MagicMock() mock_session = MagicMock() mock_session.session_id = "sid-abc" mock_state.get_session.return_value = mock_session # is_phase_processed: next_open=True (already ran), reaction_close=False (missed) def is_phase_processed(sid, date, phase): return phase == "next_open" mock_state.is_phase_processed.side_effect = is_phase_processed run_trading_calls = [] async def fake_run_trading(cmd, sessions): run_trading_calls.append(cmd) with ( patch.object(sched, "_now_et", return_value=now_et), patch.object(sched, "_is_trading_day", return_value=True), patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)), patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True), patch.object(sched, "_run_trading", side_effect=fake_run_trading), patch("apps.paper_trader.state.StateManager", return_value=mock_state), ): await sched._run_catchup() assert "run-close" in run_trading_calls # run_open should NOT re-run (market already closed) assert "run-open" not in run_trading_calls