228 lines
9.7 KiB
Python
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
|