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