# #region Test.TranslateSchedulerExecution.TestTranslateSchedulerExecution [C:3] [TYPE Module] [SEMANTICS test, translate, scheduler, execution, trigger-type, baseline] # @BRIEF Verify execute_scheduled_translation contracts — trigger_type dispatch, baseline expiry fallback. # @RELATION BINDS_TO -> [Plugin.Scheduler.ExecuteScheduledTranslation] # @TEST_EDGE: new_key_only_mode — trigger_type="new_key_only" when baseline not expired # @TEST_EDGE: baseline_expired_fallback — trigger_type="scheduled" when baseline > 90 days old # @TEST_EDGE: full_mode_default — trigger_type="scheduled" for execution_mode="full" # @TEST_EDGE: inactive_schedule — function returns early without executing # @TEST_EDGE: execution_error_handled — run marked FAILED, schedule tracking updated # @TEST_INVARIANT: trigger_type_dispatch -> VERIFIED_BY: [test_new_key_only_mode, test_baseline_expired_fallback, test_full_mode_default] from pathlib import Path import sys import asyncio sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch def _make_coro_runner(): """Mock runner whose dispatch() executes the coroutine synchronously via asyncio.run.""" r = MagicMock() r.dispatch.side_effect = lambda coro: asyncio.run(coro) return r # -- Helpers ----------------------------------------------------------------- def _make_most_recent_run(created_at_delta_days=60): """Build a MagicMock TranslationRun with given age.""" run = MagicMock() run.id = "prev-run-1" run.job_id = "job-1" run.status = "COMPLETED" run.insert_status = "succeeded" run.created_at = datetime.now(UTC) - timedelta(days=created_at_delta_days) return run def _make_mock_run(run_id="run-1", job_id="job-1"): """Build a mock TranslationRun returned by start_run.""" run = MagicMock() run.id = run_id run.job_id = job_id run.status = "PENDING" run.trigger_type = None run.total_records = 0 run.successful_records = 0 run.failed_records = 0 run.skipped_records = 0 run.insert_status = None run.superset_execution_id = None run.superset_execution_log = None run.error_message = None return run # -- Tests: trigger_type dispatch and execution modes ----------------------- # #region Test.TranslateSchedulerExecution.TestNewKeyOnlyMode [C:1] [TYPE Function] def test_new_key_only_mode(): """execution_mode="new_key_only" with 60-day-old prev run → trigger_type="new_key_only".""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-1" schedule.job_id = "job-1" schedule.is_active = True schedule.execution_mode = "new_key_only" most_recent = _make_most_recent_run(created_at_delta_days=60) # 60 days < 90 q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None # no concurrent q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = most_recent mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run() with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="new_key_only", ) assert mock_run.trigger_type == "new_key_only", ( f"Expected trigger_type='new_key_only', got '{mock_run.trigger_type}'" ) mock_orch.start_run.assert_called_once_with(job_id="job-1", is_scheduled=True) # execute_background is called (fire-and-forget) instead of execute_run via runner.run mock_orch_cls.execute_background.assert_called() mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestNewKeyOnlyMode # #region Test.TranslateSchedulerExecution.TestBaselineExpiredFallback [C:1] [TYPE Function] def test_baseline_expired_fallback(): """execution_mode="new_key_only" with >90-day-old prev run → trigger_type="scheduled" baseline fallback.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-1" schedule.job_id = "job-1" schedule.is_active = True schedule.execution_mode = "new_key_only" most_recent = _make_most_recent_run(created_at_delta_days=95) # 95 days > 90 q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = most_recent mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run() with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch with patch( "src.plugins.translate.scheduler.TranslationEventLog" ) as mock_event_log_cls: mock_event_log = MagicMock() mock_event_log_cls.return_value = mock_event_log execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="new_key_only", ) assert mock_run.trigger_type == "scheduled", ( f"Expected trigger_type='scheduled' (baseline expired), got '{mock_run.trigger_type}'" ) mock_event_log.log_event.assert_called_once() event_call = mock_event_log.log_event.call_args assert event_call.kwargs["job_id"] == "job-1" assert event_call.kwargs["event_type"] == "RUN_STARTED" assert event_call.kwargs["payload"]["reason"] == "baseline_expired" assert event_call.kwargs["payload"]["age_days"] == 95 mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestBaselineExpiredFallback # #region Test.TranslateSchedulerExecution.TestFullModeDefault [C:1] [TYPE Function] def test_full_mode_default(): """execution_mode="full" → trigger_type="scheduled" regardless of baseline age.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-1" schedule.job_id = "job-1" schedule.is_active = True schedule.execution_mode = "full" most_recent = _make_most_recent_run(created_at_delta_days=30) q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = most_recent mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run() with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="full", ) assert mock_run.trigger_type == "scheduled", ( f"Expected trigger_type='scheduled' for full mode, got '{mock_run.trigger_type}'" ) mock_orch_cls.execute_background.assert_called() mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestFullModeDefault # #region Test.TranslateSchedulerExecution.TestInactiveScheduleSkips [C:1] [TYPE Function] def test_inactive_schedule_skips(): """Inactive schedule → function returns early without executing anything.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-1" schedule.job_id = "job-1" schedule.is_active = False q1 = MagicMock() q1.filter.return_value.first.return_value = schedule mock_db.query.return_value = q1 with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="full", ) mock_orch_cls.assert_not_called() mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestInactiveScheduleSkips # #region Test.TranslateSchedulerExecution.TestScheduleNotFoundSkips [C:1] [TYPE Function] def test_schedule_not_found_skips(): """Schedule not found → function returns early.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() q1 = MagicMock() q1.filter.return_value.first.return_value = None mock_db.query.return_value = q1 with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="full", ) mock_orch_cls.assert_not_called() mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestScheduleNotFoundSkips # #region Test.TranslateSchedulerExecution.TestBaselineExpiredFullMode [C:1] [TYPE Function] def test_baseline_expired_full_mode(): """execution_mode="full" with expired baseline → trigger_type stays 'scheduled'.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-1" schedule.job_id = "job-1" schedule.is_active = True schedule.execution_mode = "full" most_recent = _make_most_recent_run(created_at_delta_days=100) # expired q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = most_recent mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run() with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch with patch( "src.plugins.translate.scheduler.TranslationEventLog" ) as mock_event_log_cls: mock_event_log = MagicMock() mock_event_log_cls.return_value = mock_event_log execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="full", ) assert mock_run.trigger_type == "scheduled" mock_event_log.log_event.assert_called_once() event_call = mock_event_log.log_event.call_args assert event_call.kwargs["payload"]["reason"] == "baseline_expired" mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestBaselineExpiredFullMode # #region Test.TranslateSchedulerExecution.TestExecutionErrorHandled [C:1] [TYPE Function] def test_execution_error_handled(): """execute_background is called even when execute_run would raise — scheduler dispatches safely.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-1" schedule.job_id = "job-1" schedule.is_active = True schedule.execution_mode = "full" q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = None mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run() with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch execute_scheduled_translation( schedule_id="sched-1", job_id="job-1", execution_mode="full", ) # Scheduler dispatches via execute_background — does NOT catch execution errors itself mock_orch_cls.execute_background.assert_called_once_with( run_id=mock_run.id, db_session_maker=mock_session_maker, config_manager=mock_config, current_user="scheduler", ) # Schedule tracking is updated at dispatch time assert schedule.last_run_at is not None mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestExecutionErrorHandled # #region Test.TranslateSchedulerExecution.TestExecutionRunStatusFailedPath [C:1] [TYPE Function] def test_execution_run_status_failed_path(): """Scheduler dispatches regardless of run.status — status is handled by background task.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-fail" schedule.job_id = "job-fail" schedule.is_active = True schedule.execution_mode = "full" most_recent = _make_most_recent_run(created_at_delta_days=30) q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = most_recent mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run(run_id="run-fail", job_id="job-fail") mock_run.status = "FAILED" # simulate run already failed with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch execute_scheduled_translation( schedule_id="sched-fail", job_id="job-fail", execution_mode="full", ) # Scheduler dispatches background task regardless of run.status mock_orch_cls.execute_background.assert_called() mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestExecutionRunStatusFailedPath # #region Test.TranslateSchedulerExecution.TestExecutionNotificationError [C:1] [TYPE Function] def test_execution_notification_error(): """Scheduler dispatches safely — notification moves to background task.""" from src.plugins.translate.scheduler import execute_scheduled_translation mock_db = MagicMock() mock_session_maker = MagicMock(return_value=mock_db) mock_config = MagicMock() schedule = MagicMock() schedule.id = "sched-notif" schedule.job_id = "job-notif" schedule.is_active = True schedule.execution_mode = "full" q1 = MagicMock() q1.filter.return_value.first.return_value = schedule q2 = MagicMock() q2.filter.return_value.order_by.return_value.first.return_value = None q3 = MagicMock() q3.filter.return_value.order_by.return_value.first.return_value = None mock_db.query.side_effect = [q1, q2, q3] mock_run = _make_mock_run(run_id="run-notif", job_id="job-notif") with patch( "src.plugins.translate.orchestrator.TranslationOrchestrator" ) as mock_orch_cls, \ patch("src.dependencies.get_async_job_runner", return_value=_make_coro_runner()), \ patch("src.core.database.SessionLocal", mock_session_maker), \ patch("src.dependencies.get_config_manager", return_value=mock_config): mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch_cls.return_value = mock_orch execute_scheduled_translation( schedule_id="sched-notif", job_id="job-notif", execution_mode="full", ) # Scheduler dispatches — notification is handled by execute_background mock_orch_cls.execute_background.assert_called() # Schedule tracking is updated at dispatch assert schedule.last_run_at is not None mock_db.close.assert_called_once() # #endregion Test.TranslateSchedulerExecution.TestExecutionNotificationError # #endregion Test.TranslateSchedulerExecution.TestTranslateSchedulerExecution