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

213 lines
9.0 KiB
Python

# #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