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

228 lines
9.7 KiB
Python

# #region Test.OrchestratorPlanner [C:3] [TYPE Module] [SEMANTICS test, translate, planner, run, creation]
# @BRIEF Tests for orchestrator_planner.py — TranslationPlanner.plan_run.
# @RELATION BINDS_TO -> [TranslationPlanner]
# @TEST_CONTRACT: plan_run -> TranslationRun | creates run with config snapshot and hashes
# @TEST_EDGE: missing_job
# @TEST_EDGE: scheduled_trigger
# @TEST_EDGE: full_translation
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, patch
import pytest
from src.plugins.translate.orchestrator_planner import TranslationPlanner
class TestTranslationPlanner:
"""TranslationPlanner.plan_run — create TranslationRun with hashes."""
def _make_job(self, **kwargs):
job = MagicMock()
job.id = "job-1"
job.source_dialect = "postgresql"
job.target_dialect = "clickhouse"
job.database_dialect = "postgresql"
job.source_datasource_id = "ds-1"
job.source_table = "source_table"
job.target_schema = "target_schema"
job.target_table = "target_table"
job.source_key_cols = ["id"]
job.target_key_cols = ["id"]
job.translation_column = "name"
job.target_column = "name_translated"
job.context_columns = ["desc"]
job.provider_id = "provider-1"
job.batch_size = 50
job.upsert_strategy = "MERGE"
job.target_languages = ["ru", "de"]
for k, v in kwargs.items():
setattr(job, k, v)
return job
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_plan_run_success(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""Creates run with all required fields."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "config_hash_1234"
mock_dict_hash.return_value = "dict_hash_5678"
planner = TranslationPlanner(db, event_log, "user")
result = planner.plan_run("job-1")
assert result.job_id == "job-1"
assert result.status == "PENDING"
assert result.trigger_type == "manual"
assert result.config_hash == "config_hash_1234"
assert result.dict_snapshot_hash == "dict_hash_5678"
assert result.key_hash is not None
assert result.created_by == "user"
assert result.config_snapshot is not None
assert result.config_snapshot["source_dialect"] == "postgresql"
assert result.config_snapshot["target_dialect"] == "clickhouse"
assert result.config_snapshot["batch_size"] == 50
assert result.config_snapshot["full_translation"] is False
db.add.assert_called_once_with(result)
db.flush.assert_called()
db.commit.assert_called()
db.refresh.assert_called_once_with(result)
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_missing_job_raises(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""Missing job raises ValueError."""
db = MagicMock()
event_log = MagicMock()
db.query.return_value.filter.return_value.first.return_value = None
planner = TranslationPlanner(db, event_log, "user")
with pytest.raises(ValueError, match="Translation job 'bad-id' not found"):
planner.plan_run("bad-id")
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_scheduled_trigger(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""is_scheduled=True sets trigger_type to 'scheduled'."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "hash1"
mock_dict_hash.return_value = "hash2"
planner = TranslationPlanner(db, event_log, "user")
result = planner.plan_run("job-1", is_scheduled=True)
assert result.trigger_type == "scheduled"
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_custom_trigger_type(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""Custom trigger_type is preserved."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "hash1"
mock_dict_hash.return_value = "hash2"
planner = TranslationPlanner(db, event_log, "user")
result = planner.plan_run("job-1", trigger_type="api_webhook")
assert result.trigger_type == "api_webhook"
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_full_translation_flag(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""full_translation=True sets flag in config_snapshot."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "hash1"
mock_dict_hash.return_value = "hash2"
planner = TranslationPlanner(db, event_log, "user")
result = planner.plan_run("job-1", full_translation=True)
assert result.config_snapshot["full_translation"] is True
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_event_logged_on_creation(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""RUN_STARTED event logged after run creation."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "ch"
mock_dict_hash.return_value = "dh"
planner = TranslationPlanner(db, event_log, "user")
result = planner.plan_run("job-1")
event_log.log_event.assert_called_once()
call_args = event_log.log_event.call_args[1]
assert call_args["event_type"] == "RUN_STARTED"
assert call_args["run_id"] == result.id
assert call_args["payload"]["config_hash"] == "ch"
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_validates_preconditions(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""Calls validate_job_preconditions with correct args."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "ch"
mock_dict_hash.return_value = "dh"
planner = TranslationPlanner(db, event_log, "user")
planner.plan_run("job-1", is_scheduled=True)
mock_validate.assert_called_once_with(job, db, is_scheduled=True)
@patch("src.plugins.translate.orchestrator_planner.compute_config_hash")
@patch("src.plugins.translate.orchestrator_planner.compute_dict_snapshot_hash")
@patch("src.plugins.translate.orchestrator_planner.validate_job_preconditions")
def test_config_snapshot_includes_all_fields(
self, mock_validate, mock_dict_hash, mock_config_hash
):
"""Config snapshot contains all relevant job fields."""
db = MagicMock()
event_log = MagicMock()
job = self._make_job()
db.query.return_value.filter.return_value.first.return_value = job
mock_config_hash.return_value = "ch"
mock_dict_hash.return_value = "dh"
planner = TranslationPlanner(db, event_log, "user")
result = planner.plan_run("job-1")
snapshot = result.config_snapshot
assert snapshot["source_dialect"] == "postgresql"
assert snapshot["target_dialect"] == "clickhouse"
assert snapshot["source_datasource_id"] == "ds-1"
assert snapshot["source_table"] == "source_table"
assert snapshot["target_schema"] == "target_schema"
assert snapshot["target_table"] == "target_table"
assert snapshot["source_key_cols"] == ["id"]
assert snapshot["translation_column"] == "name"
assert snapshot["provider_id"] == "provider-1"
assert snapshot["upsert_strategy"] == "MERGE"
# #endregion Test.OrchestratorPlanner