227 lines
8.6 KiB
Python
227 lines
8.6 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
|
|
|
|
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
|