# #region Test.Orchestrator [C:3] [TYPE Module] [SEMANTICS test, translate, orchestrator, lifecycle] # @BRIEF Tests for orchestrator.py — TranslationOrchestrator delegates to sub-components. # @RELATION BINDS_TO -> [TranslationOrchestrator] # @TEST_CONTRACT: start_run -> TranslationRun | delegates to TranslationPlanner # @TEST_CONTRACT: execute_run -> TranslationRun | delegates to TranslationStageRunner # @TEST_CONTRACT: retry_failed_batches -> TranslationRun | delegates to TranslationStageRunner # @TEST_CONTRACT: retry_insert -> TranslationRun | delegates to TranslationStageRunner # @TEST_CONTRACT: cancel_run -> TranslationRun | delegates to TranslationStageRunner # @TEST_CONTRACT: get_run_status -> dict | delegates to TranslationResultAggregator # @TEST_CONTRACT: get_run_records -> dict | delegates to TranslationResultAggregator # @TEST_CONTRACT: get_run_history -> tuple | delegates to TranslationResultAggregator # @TEST_EDGE: sql_insert_service_backward_compat # @TEST_EDGE: language_stats_updates import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) import os os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing") os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") from unittest.mock import MagicMock, AsyncMock, patch import pytest from src.plugins.translate.orchestrator import TranslationOrchestrator class TestTranslationOrchestrator: """TranslationOrchestrator — delegates all operations to sub-components.""" def _make_orchestrator(self): """Create orchestrator with mocked sub-components.""" db = MagicMock() config = MagicMock() with patch( "src.plugins.translate.orchestrator.TranslationPlanner" ) as MockPlanner, patch( "src.plugins.translate.orchestrator.TranslationStageRunner" ) as MockRunner, patch( "src.plugins.translate.orchestrator.TranslationResultAggregator" ) as MockAggregator: orch = TranslationOrchestrator(db, config, "user") return orch, db, config, MockPlanner, MockRunner, MockAggregator def test_start_run_delegates(self): """start_run delegates to TranslationPlanner.""" orch, _, _, MockPlanner, _, _ = self._make_orchestrator() mock_run = MagicMock() orch._planner = MockPlanner.return_value orch._planner.plan_run.return_value = mock_run result = orch.start_run("job-1", is_scheduled=True, trigger_type="cron", full_translation=True) orch._planner.plan_run.assert_called_once_with( job_id="job-1", is_scheduled=True, trigger_type="cron", full_translation=True, ) assert result == mock_run def test_start_run_minimal(self): """start_run with only job_id uses defaults.""" orch, _, _, MockPlanner, _, _ = self._make_orchestrator() mock_run = MagicMock() orch._planner = MockPlanner.return_value orch._planner.plan_run.return_value = mock_run result = orch.start_run("job-1") orch._planner.plan_run.assert_called_once_with( job_id="job-1", is_scheduled=False, trigger_type=None, full_translation=False, ) @pytest.mark.asyncio async def test_execute_run_delegates(self): """execute_run delegates to TranslationStageRunner.""" orch, _, _, _, MockRunner, _ = self._make_orchestrator() mock_run = MagicMock() orch._runner = MockRunner.return_value orch._runner.execute_run = AsyncMock(return_value=mock_run) progress = MagicMock() result = await orch.execute_run(mock_run, on_batch_progress=progress, skip_insert=True) orch._runner.execute_run.assert_called_once_with( run=mock_run, on_batch_progress=progress, skip_insert=True, ) assert result == mock_run @pytest.mark.asyncio async def test_retry_failed_batches_delegates(self): """retry_failed_batches delegates to TranslationStageRunner.""" orch, _, _, _, MockRunner, _ = self._make_orchestrator() mock_run = MagicMock() orch._runner = MockRunner.return_value orch._runner.retry_failed_batches = AsyncMock(return_value=mock_run) result = await orch.retry_failed_batches("run-1") orch._runner.retry_failed_batches.assert_called_once_with("run-1") @pytest.mark.asyncio async def test_retry_insert_delegates(self): """retry_insert delegates to TranslationStageRunner.""" orch, _, _, _, MockRunner, _ = self._make_orchestrator() mock_run = MagicMock() orch._runner = MockRunner.return_value orch._runner.retry_insert = AsyncMock(return_value=mock_run) result = await orch.retry_insert("run-1") orch._runner.retry_insert.assert_called_once_with("run-1") def test_cancel_run_delegates(self): """cancel_run delegates to TranslationStageRunner.""" orch, _, _, _, MockRunner, _ = self._make_orchestrator() mock_run = MagicMock() orch._runner = MockRunner.return_value orch._runner.cancel_run.return_value = mock_run result = orch.cancel_run("run-1") orch._runner.cancel_run.assert_called_once_with("run-1") def test_get_run_status_delegates(self): """get_run_status delegates to TranslationResultAggregator.""" orch, _, _, _, _, MockAggregator = self._make_orchestrator() orch._aggregator = MockAggregator.return_value orch._aggregator.get_run_status.return_value = {"status": "COMPLETED"} result = orch.get_run_status("run-1") orch._aggregator.get_run_status.assert_called_once_with("run-1") def test_get_run_records_delegates(self): """get_run_records delegates to TranslationResultAggregator.""" orch, _, _, _, _, MockAggregator = self._make_orchestrator() orch._aggregator = MockAggregator.return_value orch._aggregator.get_run_records.return_value = {"items": [], "total": 0} result = orch.get_run_records("run-1", page=2, page_size=10, status_filter="failed", deduplicate=True) orch._aggregator.get_run_records.assert_called_once_with( "run-1", 2, 10, "failed", deduplicate=True, ) def test_get_run_history_delegates(self): """get_run_history delegates to TranslationResultAggregator.""" orch, _, _, _, _, MockAggregator = self._make_orchestrator() orch._aggregator = MockAggregator.return_value orch._aggregator.get_run_history.return_value = (5, [{"id": "r1"}]) total, items = orch.get_run_history("job-1", page=1, page_size=20) orch._aggregator.get_run_history.assert_called_once_with("job-1", 1, 20) assert total == 5 assert items == [{"id": "r1"}] def test_generate_and_insert_sql_backward_compat(self): """_generate_and_insert_sql creates SQLInsertService and delegates.""" orch, db, config, _, _, _ = self._make_orchestrator() mock_job = MagicMock() mock_run = MagicMock() # SQLInsertService is imported INSIDE the method from .orchestrator_sql with patch( "src.plugins.translate.orchestrator_sql.SQLInsertService" ) as MockSQL: mock_svc = MockSQL.return_value mock_svc.generate_and_insert_sql = AsyncMock(return_value={"status": "success"}) import asyncio result = asyncio.run(orch._generate_and_insert_sql(mock_job, mock_run)) MockSQL.assert_called_once_with(db, config, orch.event_log) mock_svc.generate_and_insert_sql.assert_called_once_with(mock_job, mock_run) def test_update_language_stats_delegates(self): """_update_language_stats delegates to TranslationResultAggregator.""" orch, _, _, _, _, MockAggregator = self._make_orchestrator() orch._aggregator = MockAggregator.return_value stats_map = {"ru": MagicMock()} orch._update_language_stats("run-1", stats_map) orch._aggregator.update_language_stats.assert_called_once_with("run-1", stats_map) def test_initializes_event_log_and_components(self): """Constructor initializes all sub-components.""" db = MagicMock() config = MagicMock() with patch( "src.plugins.translate.orchestrator.TranslationPlanner" ) as MockPlanner, patch( "src.plugins.translate.orchestrator.TranslationStageRunner" ) as MockRunner, patch( "src.plugins.translate.orchestrator.TranslationResultAggregator" ) as MockAggregator: orch = TranslationOrchestrator(db, config, "user") assert orch.db == db assert orch.config_manager == config assert orch.current_user == "user" assert orch.event_log is not None MockPlanner.assert_called_once_with(db, orch.event_log, "user") MockRunner.assert_called_once_with(db, config, orch.event_log, "user") MockAggregator.assert_called_once_with(db, orch.event_log) # #endregion Test.Orchestrator