"""Unit tests for app.scheduler module.""" import pytest from app.scheduler import ( _consume_backtest_options, _consume_backtest_target_model, _parse_frequency, _resume_tickers, _last_successful, configure_scheduler, queue_backtest_options, queue_backtest_target_model, scheduler, ) def test_manual_backtest_target_model_is_one_shot(): assert queue_backtest_target_model("structural_sr") == "structural_sr" assert _consume_backtest_target_model() == "structural_sr" assert _consume_backtest_target_model() == "production_gtl" def test_manual_backtest_target_model_rejects_removed_research_arms(): with pytest.raises(ValueError, match="Unknown backtest target model"): queue_backtest_target_model("production_control") def test_manual_backtest_options_are_one_shot_and_default_back_to_weekly(): assert queue_backtest_options("structural_sr", "daily") == ( "structural_sr", "daily", ) assert _consume_backtest_options() == ("structural_sr", "daily") assert _consume_backtest_options() == ("production_gtl", "weekly") class TestParseFrequency: def test_hourly(self): assert _parse_frequency("hourly") == {"hours": 1} def test_daily(self): assert _parse_frequency("daily") == {"hours": 24} def test_case_insensitive(self): assert _parse_frequency("Hourly") == {"hours": 1} assert _parse_frequency("DAILY") == {"hours": 24} def test_weekly_maps_to_one_week(self): assert _parse_frequency("weekly") == {"weeks": 1} def test_unknown_defaults_to_daily(self): assert _parse_frequency("monthly") == {"hours": 24} assert _parse_frequency("") == {"hours": 24} class TestResumeTickers: def test_no_previous_returns_full_list(self): symbols = ["AAPL", "GOOG", "MSFT"] _last_successful["test_job"] = None result = _resume_tickers(symbols, "test_job") assert result == ["AAPL", "GOOG", "MSFT"] def test_resume_after_first(self): symbols = ["AAPL", "GOOG", "MSFT"] _last_successful["test_job"] = "AAPL" result = _resume_tickers(symbols, "test_job") # Should start from GOOG, then wrap around assert result == ["GOOG", "MSFT", "AAPL"] def test_resume_after_middle(self): symbols = ["AAPL", "GOOG", "MSFT", "TSLA"] _last_successful["test_job"] = "GOOG" result = _resume_tickers(symbols, "test_job") assert result == ["MSFT", "TSLA", "AAPL", "GOOG"] def test_resume_after_last(self): symbols = ["AAPL", "GOOG", "MSFT"] _last_successful["test_job"] = "MSFT" result = _resume_tickers(symbols, "test_job") # All already processed, wraps to full list assert result == ["AAPL", "GOOG", "MSFT"] def test_unknown_last_returns_full_list(self): symbols = ["AAPL", "GOOG", "MSFT"] _last_successful["test_job"] = "NVDA" result = _resume_tickers(symbols, "test_job") assert result == ["AAPL", "GOOG", "MSFT"] def test_empty_list(self): _last_successful["test_job"] = "AAPL" result = _resume_tickers([], "test_job") assert result == [] class TestConfigureScheduler: def test_configure_adds_all_jobs(self): # Remove any existing jobs first scheduler.remove_all_jobs() configure_scheduler() jobs = scheduler.get_jobs() job_ids = {j.id for j in jobs} assert job_ids == { "data_collector", "data_backfill", "benchmark_collector", "sentiment_collector", "fundamental_collector", "rr_scanner", "shadow_book", "ticker_universe_sync", "outcome_evaluator", "alerts", "market_regime", "regime_monitor", "event_study", "backtest", "daily_pipeline", "near_close_pipeline", "after_close_pipeline", "intraday_pipeline", } def test_configure_is_idempotent(self): scheduler.remove_all_jobs() configure_scheduler() configure_scheduler() # Should replace, not duplicate job_ids = [j.id for j in scheduler.get_jobs()] # Each ID should appear exactly once assert sorted(job_ids) == sorted([ "after_close_pipeline", "alerts", "backtest", "benchmark_collector", "daily_pipeline", "intraday_pipeline", "data_collector", "data_backfill", "fundamental_collector", "market_regime", "near_close_pipeline", "regime_monitor", "event_study", "outcome_evaluator", "rr_scanner", "sentiment_collector", "shadow_book", "ticker_universe_sync", ])