Files
ss-tools/backend/tests/plugins/translate/test_orchestrator_exec.py

215 lines
8.1 KiB
Python

# #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
# #endregion Test.OrchestratorExec