# #region Test.OrchestratorExec [C:3] [TYPE Module] [SEMANTICS test, translate, execution, run, engine] # @BRIEF Tests for orchestrator_exec.py — TranslationExecutionEngine. # @RELATION BINDS_TO -> [TranslationExecutionEngine] # @TEST_CONTRACT: execute_run -> TranslationRun | dispatches executor, handles outcomes # @TEST_CONTRACT: _init_language_stats -> dict | initializes per-language stats # @TEST_EDGE: missing_job # @TEST_EDGE: wrong_status # @TEST_EDGE: executor_failure # @TEST_EDGE: cancelled_run 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_exec import TranslationExecutionEngine class TestTranslationExecutionEngine: """TranslationExecutionEngine — execute runs, handle outcomes.""" def _make_engine(self): """Create engine with mocked deps.""" db = MagicMock() config = MagicMock() event_log = MagicMock() engine = TranslationExecutionEngine(db, config, event_log, "user") return engine, db, config, event_log def _make_run(self, **kwargs): run = MagicMock() run.id = "run-1" run.job_id = "job-1" run.status = "PENDING" for k, v in kwargs.items(): setattr(run, k, v) return run def _make_job(self, **kwargs): job = MagicMock() job.id = "job-1" job.target_languages = ["ru", "de"] job.target_dialect = "en" for k, v in kwargs.items(): setattr(job, k, v) return job @pytest.mark.asyncio async def test_execute_run_success(self): """Happy path: run executed, success completion handled.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job() db.query.return_value.filter.return_value.first.return_value = job with patch( "src.plugins.translate.orchestrator_exec.TranslationExecutor" ) as MockExecutor: mock_executor = MockExecutor.return_value mock_executor.execute_run = AsyncMock(return_value=run) with patch( "src.plugins.translate.orchestrator_exec._complete_success" ) as mock_success: mock_success.return_value = run result = await engine.execute_run(run) event_log.log_event.assert_called_once_with( job_id="job-1", run_id="run-1", event_type="TRANSLATION_PHASE_STARTED", payload={}, created_by="user", ) mock_success.assert_called_once() @pytest.mark.asyncio async def test_execute_run_missing_job_raises(self): """Missing job raises ValueError.""" engine, db, config, event_log = self._make_engine() run = self._make_run() db.query.return_value.filter.return_value.first.return_value = None with pytest.raises(ValueError, match="Job 'job-1' not found"): await engine.execute_run(run) @pytest.mark.asyncio async def test_execute_run_wrong_status_raises(self): """Non-PENDING run raises ValueError.""" engine, db, config, event_log = self._make_engine() run = self._make_run(status="RUNNING") job = self._make_job() db.query.return_value.filter.return_value.first.return_value = job with pytest.raises(ValueError, match="Cannot execute run in status 'RUNNING'"): await engine.execute_run(run) @pytest.mark.asyncio async def test_executor_failure_handled(self): """Executor exception triggers failure handler.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job() db.query.return_value.filter.return_value.first.return_value = job with patch( "src.plugins.translate.orchestrator_exec.TranslationExecutor" ) as MockExecutor: mock_executor = MockExecutor.return_value mock_executor.execute_run = AsyncMock(side_effect=ValueError("LLM failed")) with patch( "src.plugins.translate.orchestrator_exec._handle_executor_failure" ) as mock_failure: mock_failure.return_value = run result = await engine.execute_run(run) mock_failure.assert_called_once() assert result == run @pytest.mark.asyncio async def test_cancelled_run_handled(self): """Run with CANCELLED status after executor triggers complete_cancelled.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job() db.query.return_value.filter.return_value.first.return_value = job with patch( "src.plugins.translate.orchestrator_exec.TranslationExecutor" ) as MockExecutor: cancelled_run = self._make_run(status="CANCELLED") mock_executor = MockExecutor.return_value mock_executor.execute_run = AsyncMock(return_value=cancelled_run) with patch( "src.plugins.translate.orchestrator_exec._complete_cancelled" ) as mock_cancelled: mock_cancelled.return_value = cancelled_run result = await engine.execute_run(run) mock_cancelled.assert_called_once() assert result == cancelled_run @pytest.mark.asyncio async def test_executor_passed_progress_callback(self): """Progress callback and skip_insert passed to executor.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job() db.query.return_value.filter.return_value.first.return_value = job progress_cb = MagicMock() with patch( "src.plugins.translate.orchestrator_exec.TranslationExecutor" ) as MockExecutor: mock_executor = MockExecutor.return_value mock_executor.execute_run = AsyncMock(return_value=run) with patch( "src.plugins.translate.orchestrator_exec._complete_success" ) as mock_success: mock_success.return_value = run result = await engine.execute_run(run, on_batch_progress=progress_cb, skip_insert=True) # Verify executor was created with callback MockExecutor.assert_called_once() _, kwargs = MockExecutor.call_args assert kwargs["on_batch_progress"] == progress_cb def test_init_language_stats(self): """_init_language_stats creates stats for each target language.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job(target_languages=["ru", "de"]) result = engine._init_language_stats(run, job) assert len(result) == 2 assert "ru" in result assert "de" in result assert result["ru"].language_code == "ru" assert result["ru"].run_id == "run-1" assert db.add.call_count == 2 db.flush.assert_called_once() def test_init_language_stats_string_languages(self): """String target_languages is converted to list.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job(target_languages=None, target_dialect="fr") result = engine._init_language_stats(run, job) assert len(result) == 1 assert "fr" in result def test_init_language_stats_default_fallback(self): """No target_languages and no target_dialect defaults to 'en'.""" engine, db, config, event_log = self._make_engine() run = self._make_run() job = self._make_job(target_languages=None, target_dialect=None) result = engine._init_language_stats(run, job) assert len(result) == 1 assert "en" in result def test_init_language_stats_string_language_not_list(self): """String target_languages (not a list) is wrapped in [str()] (line 108).""" engine, db, config, event_log = self._make_engine() run = self._make_run() # target_languages as a plain string (not a list) — line 107 isinstance check fails -> line 108 job = self._make_job(target_languages="ru") result = engine._init_language_stats(run, job) assert len(result) == 1 assert "ru" in result # #endregion Test.OrchestratorExec