# #region Test.Scheduler [C:3] [TYPE Module] [SEMANTICS test,scheduler,core] # @BRIEF Unit tests for SchedulerModule — SchedulerService lifecycle, job management, and ThrottledSchedulerConfigurator. # @RELATION BINDS_TO -> [SchedulerModule] # @TEST_EDGE: empty_queue -> _trigger_backup with no active tasks creates a new task # @TEST_EDGE: duplicate_backup -> _trigger_backup skips when backup already running # @TEST_EDGE: invalid_cron -> add_backup_job handles invalid cron expression gracefully # @TEST_EDGE: translation_load_failure -> load_schedules continues when translation DB query fails # @TEST_EDGE: validation_load_failure -> load_schedules continues when validation DB query fails # @TEST_EDGE: policy_not_found -> reload_validation_policy removes job when policy missing # @TEST_EDGE: policy_inactive -> reload_validation_policy removes job when policy inactive # @TEST_EDGE: policy_no_schedule -> reload_validation_policy skips when no schedule_days/window # @TEST_EDGE: zero_window -> calculate_schedule returns start_dt repeated for zero-length window # @TEST_EDGE: midnight_crossing -> calculate_schedule handles window crossing midnight # @TEST_EDGE: single_dashboard -> calculate_schedule returns single start_dt for one dashboard import asyncio from datetime import date, datetime, time, timedelta from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock import pytest # ── Helpers (C1 — anchor pair only) ── # #region _build_config_manager [C:1] [TYPE Function] def _build_config_manager(environments=None, app_timezone="Europe/Moscow"): """Build a mock ConfigManager with configurable environments.""" cm = MagicMock() config = MagicMock() settings = MagicMock() settings.app_timezone = app_timezone config.settings = settings config.environments = environments or [] cm.get_config.return_value = config return cm # #endregion _build_config_manager # #region _build_env [C:1] [TYPE Function] def _build_env(env_id="env-1", cron="0 2 * * *", enabled=True): """Build a mock Environment with backup_schedule.""" env = MagicMock() env.id = env_id sched = MagicMock() sched.enabled = enabled sched.cron_expression = cron env.backup_schedule = sched return env # #endregion _build_env # #region _build_task [C:1] [TYPE Function] def _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-1"): """Build a mock TaskRecord.""" task = MagicMock() task.plugin_id = plugin_id task.status = status task.params = {"environment_id": environment_id} return task # #endregion _build_task # #region _build_policy [C:1] [TYPE Function] def _build_policy(policy_id="pol-1", is_active=True, schedule_days=None, window_start=None, window_end=None, dashboard_ids=None, environment_id="env-1", provider_id="prov-1"): """Build a mock ValidationPolicy.""" policy = MagicMock() policy.id = policy_id policy.is_active = is_active policy.schedule_days = schedule_days policy.window_start = window_start policy.window_end = window_end policy.dashboard_ids = dashboard_ids policy.environment_id = environment_id policy.provider_id = provider_id return policy # #endregion _build_policy # ── Fixtures ── @pytest.fixture def mock_task_manager(): tm = MagicMock() tm.get_tasks.return_value = [] tm.create_task = AsyncMock(return_value=MagicMock()) return tm @pytest.fixture def mock_config_manager(): return _build_config_manager() @pytest.fixture def mock_scheduler(): """Patch BackgroundScheduler so no real threads are started.""" with patch("src.core.scheduler.BackgroundScheduler") as cls: sched_instance = MagicMock() sched_instance.running = False cls.return_value = sched_instance yield sched_instance @pytest.fixture def service(mock_task_manager, mock_config_manager, mock_scheduler): """Construct a SchedulerService with all external deps mocked.""" # Create a fresh event loop to avoid "no current event loop" errors # when the scheduler test runs after async tests in the same session. loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.asyncio.get_event_loop", return_value=loop): from src.core.scheduler import SchedulerService svc = SchedulerService(mock_task_manager, mock_config_manager) yield svc loop.close() # ══════════════════════════════════════════════════════════════ # SchedulerService.__init__ (lines 28-32) # ══════════════════════════════════════════════════════════════ # #region test_init_stores_deps [C:2] [TYPE Function] # @BRIEF __init__ stores task_manager, config_manager, creates scheduler and loop. def test_init_stores_deps(service, mock_task_manager, mock_config_manager, mock_scheduler): assert service.task_manager is mock_task_manager assert service.config_manager is mock_config_manager assert service.scheduler is mock_scheduler assert service.loop is not None # #endregion test_init_stores_deps # ══════════════════════════════════════════════════════════════ # SchedulerService.start (lines 40-44) # ══════════════════════════════════════════════════════════════ # #region test_start_when_not_running [C:2] [TYPE Function] # @BRIEF start() starts scheduler and loads schedules when not already running. def test_start_when_not_running(service, mock_scheduler): mock_scheduler.running = False with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch.object(service, "load_schedules") as mock_load: service.start() mock_scheduler.start.assert_called_once() mock_load.assert_called_once() # #endregion test_start_when_not_running # #region test_start_when_already_running [C:2] [TYPE Function] # @BRIEF start() is a no-op when scheduler is already running. def test_start_when_already_running(service, mock_scheduler): mock_scheduler.running = True with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch.object(service, "load_schedules") as mock_load: service.start() mock_scheduler.start.assert_not_called() mock_load.assert_not_called() # #endregion test_start_when_already_running # ══════════════════════════════════════════════════════════════ # SchedulerService.stop (lines 52-55) # ══════════════════════════════════════════════════════════════ # #region test_stop_when_running [C:2] [TYPE Function] # @BRIEF stop() shuts down scheduler when it is running. def test_stop_when_running(service, mock_scheduler): mock_scheduler.running = True with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) service.stop() mock_scheduler.shutdown.assert_called_once() # #endregion test_stop_when_running # #region test_stop_when_not_running [C:2] [TYPE Function] # @BRIEF stop() is a no-op when scheduler is not running. def test_stop_when_not_running(service, mock_scheduler): mock_scheduler.running = False with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) service.stop() mock_scheduler.shutdown.assert_not_called() # #endregion test_stop_when_not_running # ══════════════════════════════════════════════════════════════ # SchedulerService.load_schedules (lines 63-130) # ══════════════════════════════════════════════════════════════ # #region test_load_schedules_backup_jobs [C:2] [TYPE Function] # @BRIEF load_schedules registers backup jobs for enabled environments. def test_load_schedules_backup_jobs(service, mock_scheduler, mock_config_manager): env1 = _build_env("env-1", "0 2 * * *", enabled=True) env2 = _build_env("env-2", "0 3 * * *", enabled=False) cm = _build_config_manager(environments=[env1, env2]) service.config_manager = cm with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock # No translation schedules, no validation policies db_mock.query.return_value.filter.return_value.all.return_value = [] with patch.object(service, "add_backup_job") as mock_add: service.load_schedules() # Only env-1 has enabled schedule mock_add.assert_called_once_with("env-1", "0 2 * * *") # #endregion test_load_schedules_backup_jobs # #region test_load_schedules_translation_exception [C:2] [TYPE Function] # @BRIEF load_schedules catches exception when loading translation schedules (lines 93-94). def test_load_schedules_translation_exception(service, mock_config_manager): cm = _build_config_manager(environments=[]) service.config_manager = cm with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=RuntimeError("DB connection lost")): # Should not raise — exception is caught at line 93 service.load_schedules() # #endregion test_load_schedules_translation_exception # #region test_load_schedules_validation_exception [C:2] [TYPE Function] # @BRIEF load_schedules catches exception when loading validation schedules (lines 128-129). def test_load_schedules_validation_exception(service, mock_config_manager): cm = _build_config_manager(environments=[]) service.config_manager = cm call_count = 0 original_session_local = None def session_side_effect(): nonlocal call_count call_count += 1 if call_count == 1: # First call (translation) — return empty mock db = MagicMock() db.query.return_value.filter.return_value.all.return_value = [] return db else: # Second call (validation) — raise raise RuntimeError("Validation DB error") with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=session_side_effect): # Should not raise — exception caught at line 128 service.load_schedules() # #endregion test_load_schedules_validation_exception # #region test_load_schedules_translation_schedules_success [C:2] [TYPE Function] # @BRIEF load_schedules registers active translation schedules from DB (lines 82, 90). def test_load_schedules_translation_schedules_success(service, mock_config_manager): cm = _build_config_manager(environments=[], app_timezone="UTC") service.config_manager = cm # Build a mock TranslationSchedule row ts = MagicMock() ts.id = "ts-1" ts.job_id = "job-ts-1" ts.cron_expression = "*/10 * * * *" ts.timezone = "US/Pacific" ts.execution_mode = "incremental" call_count = 0 def session_factory(): nonlocal call_count call_count += 1 db = MagicMock() if call_count == 1: # Translation query — return one active schedule db.query.return_value.filter.return_value.all.return_value = [ts] else: # Validation query — empty db.query.return_value.filter.return_value.all.return_value = [] return db with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=session_factory): with patch.object(service, "add_translation_job") as mock_add_t: with patch.object(service, "add_validation_job") as mock_add_v: service.load_schedules() mock_add_t.assert_called_once_with( schedule_id="ts-1", job_id="job-ts-1", cron_expression="*/10 * * * *", timezone="US/Pacific", execution_mode="incremental", ) mock_add_v.assert_not_called() # #endregion test_load_schedules_translation_schedules_success # #region test_load_schedules_validation_policy_skip_empty_days [C:2] [TYPE Function] # @BRIEF load_schedules skips validation policies with empty schedule_days (line 112). def test_load_schedules_validation_policy_skip_empty_days(service, mock_config_manager): cm = _build_config_manager(environments=[], app_timezone="UTC") service.config_manager = cm policy_no_days = _build_policy("pol-1", is_active=True, schedule_days=[], window_start=time(10, 0)) policy_none_days = _build_policy("pol-2", is_active=True, schedule_days=None, window_start=time(10, 0)) call_count = 0 def session_factory(): nonlocal call_count call_count += 1 db = MagicMock() if call_count == 1: # Translation query — empty db.query.return_value.filter.return_value.all.return_value = [] else: # Validation query — policies with empty/None schedule_days db.query.return_value.filter.return_value.all.return_value = [policy_no_days, policy_none_days] return db with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=session_factory): with patch.object(service, "add_validation_job") as mock_add: service.load_schedules() # Neither policy should be registered (both have empty/None schedule_days) mock_add.assert_not_called() # #endregion test_load_schedules_validation_policy_skip_empty_days # #region test_load_schedules_validation_policy_skip_no_window [C:2] [TYPE Function] # @BRIEF load_schedules skips validation policies with None window_start (line 115). def test_load_schedules_validation_policy_skip_no_window(service, mock_config_manager): cm = _build_config_manager(environments=[], app_timezone="UTC") service.config_manager = cm policy_no_ws = _build_policy("pol-3", is_active=True, schedule_days=[1, 3], window_start=None) call_count = 0 def session_factory(): nonlocal call_count call_count += 1 db = MagicMock() if call_count == 1: db.query.return_value.filter.return_value.all.return_value = [] else: db.query.return_value.filter.return_value.all.return_value = [policy_no_ws] return db with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=session_factory): with patch.object(service, "add_validation_job") as mock_add: service.load_schedules() mock_add.assert_not_called() # #endregion test_load_schedules_validation_policy_skip_no_window # #region test_load_schedules_validation_policy_success [C:2] [TYPE Function] # @BRIEF load_schedules registers valid validation policies with cron expression. def test_load_schedules_validation_policy_success(service, mock_config_manager): cm = _build_config_manager(environments=[], app_timezone="UTC") service.config_manager = cm ws = time(10, 30) policy = _build_policy("pol-ok", is_active=True, schedule_days=[1, 3, 5], window_start=ws) call_count = 0 def session_factory(): nonlocal call_count call_count += 1 db = MagicMock() if call_count == 1: db.query.return_value.filter.return_value.all.return_value = [] else: db.query.return_value.filter.return_value.all.return_value = [policy] return db with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=session_factory): with patch.object(service, "add_validation_job") as mock_add: service.load_schedules() # cron: minute=30 hour=10 * * 1,3,5 mock_add.assert_called_once_with(policy_id="pol-ok", cron_expression="30 10 * * 1,3,5", app_timezone="UTC") # #endregion test_load_schedules_validation_policy_success # ══════════════════════════════════════════════════════════════ # SchedulerService.add_backup_job (lines 139-157) # ══════════════════════════════════════════════════════════════ # #region test_add_backup_job_success [C:2] [TYPE Function] # @BRIEF add_backup_job registers a job with APScheduler. def test_add_backup_job_success(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.CronTrigger") as mock_trigger: mock_trigger.from_crontab.return_value = "trigger_obj" service.add_backup_job("env-1", "0 2 * * *") mock_scheduler.add_job.assert_called_once() call_kwargs = mock_scheduler.add_job.call_args assert call_kwargs.kwargs["id"] == "backup_env-1" assert call_kwargs.kwargs["replace_existing"] is True # #endregion test_add_backup_job_success # #region test_add_backup_job_invalid_cron [C:2] [TYPE Function] # @BRIEF add_backup_job handles invalid cron expression without raising (lines 156-157). def test_add_backup_job_invalid_cron(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.CronTrigger") as mock_trigger: mock_trigger.from_crontab.side_effect = ValueError("Invalid cron") # Should not raise service.add_backup_job("env-bad", "invalid_cron") mock_scheduler.add_job.assert_not_called() # #endregion test_add_backup_job_invalid_cron # ══════════════════════════════════════════════════════════════ # SchedulerService.add_translation_job (lines 167-193) # ══════════════════════════════════════════════════════════════ # #region test_add_translation_job_success [C:2] [TYPE Function] # @BRIEF add_translation_job registers a translation job with APScheduler. def test_add_translation_job_success(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.CronTrigger") as mock_trigger: mock_trigger.from_crontab.return_value = "trigger_obj" service.add_translation_job("sched-1", "job-1", "*/5 * * * *", "US/Eastern", "incremental") mock_scheduler.add_job.assert_called_once() call_kwargs = mock_scheduler.add_job.call_args assert call_kwargs.kwargs["id"] == "translate_sched-1" # #endregion test_add_translation_job_success # #region test_add_translation_job_exception [C:2] [TYPE Function] # @BRIEF add_translation_job handles registration failure gracefully (lines 189-190). def test_add_translation_job_exception(service, mock_scheduler): mock_scheduler.add_job.side_effect = RuntimeError("APScheduler failure") with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.CronTrigger") as mock_trigger: mock_trigger.from_crontab.return_value = "trigger_obj" # Should not raise service.add_translation_job("sched-2", "job-2", "0 0 * * *") # #endregion test_add_translation_job_exception # ══════════════════════════════════════════════════════════════ # SchedulerService.remove_translation_job # ══════════════════════════════════════════════════════════════ # #region test_remove_translation_job_exists [C:2] [TYPE Function] # @BRIEF remove_translation_job removes an existing job. def test_remove_translation_job_exists(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) service.remove_translation_job("sched-1") mock_scheduler.remove_job.assert_called_once_with("translate_sched-1") # #endregion test_remove_translation_job_exists # #region test_remove_translation_job_not_found [C:2] [TYPE Function] # @BRIEF remove_translation_job silently handles missing job. def test_remove_translation_job_not_found(service, mock_scheduler): mock_scheduler.remove_job.side_effect = Exception("Job not found") with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) # Should not raise service.remove_translation_job("sched-missing") # #endregion test_remove_translation_job_not_found # ══════════════════════════════════════════════════════════════ # SchedulerService._trigger_backup (lines 218-241) # ══════════════════════════════════════════════════════════════ # #region test_trigger_backup_no_active_tasks [C:2] [TYPE Function] # @BRIEF _trigger_backup creates a task when no backup is running (lines 219-236). def test_trigger_backup_no_active_tasks(service, mock_task_manager): mock_task_manager.get_tasks.return_value = [] with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run: service._trigger_backup("env-1") # create_task is called and its coroutine passed to run_coroutine_threadsafe mock_run.assert_called_once() mock_task_manager.create_task.assert_called_once_with( "superset-backup", {"environment_id": "env-1"} ) # #endregion test_trigger_backup_no_active_tasks # #region test_trigger_backup_already_running [C:2] [TYPE Function] # @BRIEF _trigger_backup skips when a backup is already running for the environment. def test_trigger_backup_already_running(service, mock_task_manager): running_task = _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-1") mock_task_manager.get_tasks.return_value = [running_task] with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run: service._trigger_backup("env-1") mock_run.assert_not_called() # #endregion test_trigger_backup_already_running # #region test_trigger_backup_pending_status [C:2] [TYPE Function] # @BRIEF _trigger_backup skips when a backup is in PENDING status for the environment. def test_trigger_backup_pending_status(service, mock_task_manager): pending_task = _build_task(plugin_id="superset-backup", status="PENDING", environment_id="env-1") mock_task_manager.get_tasks.return_value = [pending_task] with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run: service._trigger_backup("env-1") mock_run.assert_not_called() # #endregion test_trigger_backup_pending_status # #region test_trigger_backup_different_env [C:2] [TYPE Function] # @BRIEF _trigger_backup proceeds when running backup is for a different environment. def test_trigger_backup_different_env(service, mock_task_manager): other_task = _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-2") mock_task_manager.get_tasks.return_value = [other_task] with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run: service._trigger_backup("env-1") mock_run.assert_called_once() # #endregion test_trigger_backup_different_env # #region test_trigger_backup_other_plugin [C:2] [TYPE Function] # @BRIEF _trigger_backup proceeds when running task is a different plugin. def test_trigger_backup_other_plugin(service, mock_task_manager): other_plugin_task = _build_task(plugin_id="other-plugin", status="RUNNING", environment_id="env-1") mock_task_manager.get_tasks.return_value = [other_plugin_task] with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run: service._trigger_backup("env-1") mock_run.assert_called_once() # #endregion test_trigger_backup_other_plugin # ══════════════════════════════════════════════════════════════ # SchedulerService.add_validation_job # ══════════════════════════════════════════════════════════════ # #region test_add_validation_job_success [C:2] [TYPE Function] # @BRIEF add_validation_job registers a validation job with APScheduler. def test_add_validation_job_success(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.CronTrigger") as mock_trigger: mock_trigger.from_crontab.return_value = "trigger_obj" service.add_validation_job("pol-1", "30 10 * * 1,3", app_timezone="UTC") mock_scheduler.add_job.assert_called_once() call_kwargs = mock_scheduler.add_job.call_args assert call_kwargs.kwargs["id"] == "validation_pol-1" # #endregion test_add_validation_job_success # #region test_add_validation_job_exception [C:2] [TYPE Function] # @BRIEF add_validation_job handles registration failure gracefully. def test_add_validation_job_exception(service, mock_scheduler): mock_scheduler.add_job.side_effect = RuntimeError("Scheduler error") with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.CronTrigger") as mock_trigger: mock_trigger.from_crontab.return_value = "trigger_obj" # Should not raise service.add_validation_job("pol-bad", "30 10 * * 1") # #endregion test_add_validation_job_exception # ══════════════════════════════════════════════════════════════ # SchedulerService.remove_validation_job # ══════════════════════════════════════════════════════════════ # #region test_remove_validation_job_exists [C:2] [TYPE Function] # @BRIEF remove_validation_job removes an existing job. def test_remove_validation_job_exists(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) service.remove_validation_job("pol-1") mock_scheduler.remove_job.assert_called_once_with("validation_pol-1") # #endregion test_remove_validation_job_exists # #region test_remove_validation_job_not_found [C:2] [TYPE Function] # @BRIEF remove_validation_job silently handles missing job. def test_remove_validation_job_not_found(service, mock_scheduler): mock_scheduler.remove_job.side_effect = Exception("Not found") with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) # Should not raise service.remove_validation_job("pol-missing") # #endregion test_remove_validation_job_not_found # ══════════════════════════════════════════════════════════════ # SchedulerService.reload_validation_policy (lines 300-331) # ══════════════════════════════════════════════════════════════ # #region test_reload_validation_policy_not_found [C:2] [TYPE Function] # @BRIEF reload_validation_policy removes job when policy not found in DB (lines 315-316). def test_reload_validation_policy_not_found(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = None with patch.object(service, "remove_validation_job") as mock_remove: with patch.object(service, "add_validation_job") as mock_add: service.reload_validation_policy("pol-missing") mock_remove.assert_called_once_with("pol-missing") mock_add.assert_not_called() # #endregion test_reload_validation_policy_not_found # #region test_reload_validation_policy_inactive [C:2] [TYPE Function] # @BRIEF reload_validation_policy removes job when policy is inactive (lines 319-320). def test_reload_validation_policy_inactive(service, mock_scheduler): policy = _build_policy("pol-inactive", is_active=False) with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = policy with patch.object(service, "remove_validation_job") as mock_remove: with patch.object(service, "add_validation_job") as mock_add: service.reload_validation_policy("pol-inactive") mock_remove.assert_called_once_with("pol-inactive") mock_add.assert_not_called() # #endregion test_reload_validation_policy_inactive # #region test_reload_validation_policy_no_schedule [C:2] [TYPE Function] # @BRIEF reload_validation_policy skips registration when no schedule_days or window_start (lines 327-328 area). def test_reload_validation_policy_no_schedule(service, mock_scheduler): policy = _build_policy("pol-nosched", is_active=True, schedule_days=[], window_start=None) with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = policy with patch.object(service, "remove_validation_job") as mock_remove: with patch.object(service, "add_validation_job") as mock_add: service.reload_validation_policy("pol-nosched") mock_remove.assert_called_once_with("pol-nosched") mock_add.assert_not_called() # #endregion test_reload_validation_policy_no_schedule # #region test_reload_validation_policy_success [C:2] [TYPE Function] # @BRIEF reload_validation_policy removes old and registers new job for active policy with schedule. def test_reload_validation_policy_success(service, mock_scheduler): ws = time(14, 0) policy = _build_policy("pol-ok", is_active=True, schedule_days=[0, 2, 4], window_start=ws) with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = policy with patch.object(service, "remove_validation_job") as mock_remove: with patch.object(service, "add_validation_job") as mock_add: service.reload_validation_policy("pol-ok", app_timezone="UTC") mock_remove.assert_called_once_with("pol-ok") # cron: minute=0 hour=14 * * 0,2,4 mock_add.assert_called_once_with("pol-ok", "0 14 * * 0,2,4", app_timezone="UTC") # #endregion test_reload_validation_policy_success # #region test_reload_validation_policy_exception [C:2] [TYPE Function] # @BRIEF reload_validation_policy handles DB exception gracefully (lines 327-328). def test_reload_validation_policy_exception(service, mock_scheduler): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal", side_effect=RuntimeError("DB down")): with patch.object(service, "remove_validation_job"): # Should not raise service.reload_validation_policy("pol-err") # #endregion test_reload_validation_policy_exception # ══════════════════════════════════════════════════════════════ # SchedulerService._trigger_validation (lines 338-391) # ══════════════════════════════════════════════════════════════ # #region test_trigger_validation_policy_not_found [C:2] [TYPE Function] # @BRIEF _trigger_validation returns early when policy not found in DB. def test_trigger_validation_policy_not_found(service, mock_task_manager): with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = None # Should not raise service._trigger_validation("pol-missing") db_mock.close.assert_called_once() # #endregion test_trigger_validation_policy_not_found # #region test_trigger_validation_policy_inactive [C:2] [TYPE Function] # @BRIEF _trigger_validation returns early when policy is inactive. def test_trigger_validation_policy_inactive(service, mock_task_manager): policy = _build_policy("pol-inactive", is_active=False) with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = policy service._trigger_validation("pol-inactive") mock_task_manager.create_task.assert_not_called() # #endregion test_trigger_validation_policy_inactive # #region test_trigger_validation_no_dashboards [C:2] [TYPE Function] # @BRIEF _trigger_validation returns early when policy has no dashboard_ids. def test_trigger_validation_no_dashboards(service, mock_task_manager): policy = _build_policy("pol-nodash", is_active=True, dashboard_ids=[], window_start=time(10, 0), window_end=time(11, 0)) with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = policy service._trigger_validation("pol-nodash") mock_task_manager.create_task.assert_not_called() # #endregion test_trigger_validation_no_dashboards # #region test_trigger_validation_success [C:2] [TYPE Function] # @BRIEF _trigger_validation spawns tasks for each dashboard in the policy. def test_trigger_validation_success(service, mock_task_manager): dash_ids = ["dash-1", "dash-2"] policy = _build_policy( "pol-ok", is_active=True, dashboard_ids=dash_ids, window_start=time(10, 0), window_end=time(11, 0), environment_id="env-1", provider_id="prov-1", ) with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock db_mock.query.return_value.filter.return_value.first.return_value = policy with patch("src.core.scheduler.ThrottledSchedulerConfigurator") as mock_cfg: mock_cfg.calculate_schedule.return_value = [ datetime(2026, 6, 11, 10, 0), datetime(2026, 6, 11, 11, 0), ] with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run: service._trigger_validation("pol-ok") # Two dashboards → two run_coroutine_threadsafe calls assert mock_run.call_count == 2 # #endregion test_trigger_validation_success # #region test_trigger_validation_exception [C:2] [TYPE Function] # @BRIEF _trigger_validation handles exception gracefully (lines 385-386). def test_trigger_validation_exception(service, mock_task_manager): with patch("src.core.scheduler.seed_trace_id"): with patch("src.core.scheduler.belief_scope") as mock_bs: mock_bs.return_value.__enter__ = MagicMock() mock_bs.return_value.__exit__ = MagicMock(return_value=False) with patch("src.core.scheduler.SessionLocal") as mock_session: db_mock = MagicMock() mock_session.return_value = db_mock # Raise inside the try block (query, not SessionLocal itself) db_mock.query.side_effect = RuntimeError("DB crash") # Should not raise service._trigger_validation("pol-err") db_mock.close.assert_called_once() # #endregion test_trigger_validation_exception # ══════════════════════════════════════════════════════════════ # ThrottledSchedulerConfigurator.calculate_schedule (lines 410-445) # ══════════════════════════════════════════════════════════════ # #region test_calculate_schedule_empty [C:2] [TYPE Function] # @BRIEF calculate_schedule returns empty list for empty dashboard_ids (line 417). def test_calculate_schedule_empty(): from src.core.scheduler import ThrottledSchedulerConfigurator result = ThrottledSchedulerConfigurator.calculate_schedule( window_start=time(10, 0), window_end=time(11, 0), dashboard_ids=[], current_date=date(2026, 6, 11), ) assert result == [] # #endregion test_calculate_schedule_empty # #region test_calculate_schedule_single_dashboard [C:2] [TYPE Function] # @BRIEF calculate_schedule returns single start_dt for one dashboard (line 434). def test_calculate_schedule_single_dashboard(): from src.core.scheduler import ThrottledSchedulerConfigurator result = ThrottledSchedulerConfigurator.calculate_schedule( window_start=time(10, 0), window_end=time(11, 0), dashboard_ids=["dash-1"], current_date=date(2026, 6, 11), ) assert len(result) == 1 assert result[0] == datetime(2026, 6, 11, 10, 0) # #endregion test_calculate_schedule_single_dashboard # #region test_calculate_schedule_multiple [C:2] [TYPE Function] # @BRIEF calculate_schedule distributes tasks evenly across the window. def test_calculate_schedule_multiple(): from src.core.scheduler import ThrottledSchedulerConfigurator result = ThrottledSchedulerConfigurator.calculate_schedule( window_start=time(10, 0), window_end=time(11, 0), dashboard_ids=["d1", "d2", "d3"], current_date=date(2026, 6, 11), ) assert len(result) == 3 # First at 10:00, last at 11:00, middle at 10:30 assert result[0] == datetime(2026, 6, 11, 10, 0) assert result[1] == datetime(2026, 6, 11, 10, 30) assert result[2] == datetime(2026, 6, 11, 11, 0) # #endregion test_calculate_schedule_multiple # #region test_calculate_schedule_midnight_crossing [C:2] [TYPE Function] # @BRIEF calculate_schedule handles window crossing midnight (line 422). def test_calculate_schedule_midnight_crossing(): from src.core.scheduler import ThrottledSchedulerConfigurator result = ThrottledSchedulerConfigurator.calculate_schedule( window_start=time(23, 0), window_end=time(1, 0), dashboard_ids=["d1", "d2", "d3"], current_date=date(2026, 6, 11), ) assert len(result) == 3 # Window: 23:00 → 01:00 next day = 2 hours = 7200s # Interval: 7200 / 2 = 3600s = 1h assert result[0] == datetime(2026, 6, 11, 23, 0) assert result[1] == datetime(2026, 6, 12, 0, 0) assert result[2] == datetime(2026, 6, 12, 1, 0) # #endregion test_calculate_schedule_midnight_crossing # #region test_calculate_schedule_zero_window [C:2] [TYPE Function] # @BRIEF calculate_schedule returns start_dt repeated for zero-length window (lines 426-429). def test_calculate_schedule_zero_window(): from src.core.scheduler import ThrottledSchedulerConfigurator result = ThrottledSchedulerConfigurator.calculate_schedule( window_start=time(10, 0), window_end=time(10, 0), dashboard_ids=["d1", "d2", "d3"], current_date=date(2026, 6, 11), ) assert len(result) == 3 expected = datetime(2026, 6, 11, 10, 0) assert all(t == expected for t in result) # #endregion test_calculate_schedule_zero_window # #region test_calculate_schedule_small_window_warning [C:2] [TYPE Function] # @BRIEF calculate_schedule warns when interval < 1s (lines 439+). def test_calculate_schedule_small_window_warning(): from src.core.scheduler import ThrottledSchedulerConfigurator # 10 dashboards in 5 seconds → interval = 5/9 ≈ 0.56s < 1s result = ThrottledSchedulerConfigurator.calculate_schedule( window_start=time(10, 0, 0), window_end=time(10, 0, 5), dashboard_ids=[f"d{i}" for i in range(10)], current_date=date(2026, 6, 11), ) assert len(result) == 10 # First should be at start, last should be within the window assert result[0] == datetime(2026, 6, 11, 10, 0, 0) # #endregion test_calculate_schedule_small_window_warning # #endregion Test.Scheduler