- 10 translate plugin test files (100% coverage on 12 modules) - assistant/handler tools: 85+ tests covering dispatch, registry, resolvers, routes, llm_planner, 13 tool handlers - clean release: artifact_catalog_loader, mappers, approval, publication tests - API routes: translate_helpers, validation_service extensions, datasets to 100% - notifications: providers/service tests - services: profile_preference_service - docs/orthogonal-test-report.md — full speckit.tests audit - Fixes: 3 git_base async mock failures, 4 assistant handler permission-check patches - .gitignore: coverage artifacts
167 lines
6.9 KiB
Python
167 lines
6.9 KiB
Python
# #region Test.OrchestratorConfig [C:3] [TYPE Module] [SEMANTICS test, translate, config, hash, snapshot]
|
|
# @BRIEF Tests for orchestrator_config.py — compute_config_hash, compute_dict_snapshot_hash.
|
|
# @RELATION BINDS_TO -> [orchestrator_config]
|
|
# @TEST_CONTRACT: compute_config_hash -> str | deterministic 16-char hash from job config
|
|
# @TEST_CONTRACT: compute_dict_snapshot_hash -> str | hash from dictionary entries
|
|
|
|
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 datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.plugins.translate.orchestrator_config import (
|
|
compute_config_hash,
|
|
compute_dict_snapshot_hash,
|
|
)
|
|
|
|
|
|
class TestComputeConfigHash:
|
|
"""compute_config_hash — deterministic hash from job config."""
|
|
|
|
def _make_job(self, overrides: dict | None = None) -> MagicMock:
|
|
"""Create a mock job with default config."""
|
|
job = MagicMock()
|
|
job.source_dialect = "postgresql"
|
|
job.target_dialect = "clickhouse"
|
|
job.source_datasource_id = "ds-1"
|
|
job.translation_column = "name"
|
|
job.context_columns = ["desc"]
|
|
job.target_languages = ["ru", "de"]
|
|
job.provider_id = "provider-1"
|
|
job.batch_size = 50
|
|
job.upsert_strategy = "MERGE"
|
|
if overrides:
|
|
for k, v in overrides.items():
|
|
setattr(job, k, v)
|
|
return job
|
|
|
|
def test_hash_length(self):
|
|
"""Hash is 16 characters."""
|
|
job = self._make_job()
|
|
h = compute_config_hash(job)
|
|
assert isinstance(h, str)
|
|
assert len(h) == 16
|
|
|
|
def test_deterministic(self):
|
|
"""Same config produces same hash."""
|
|
job1 = self._make_job()
|
|
job2 = self._make_job()
|
|
assert compute_config_hash(job1) == compute_config_hash(job2)
|
|
|
|
def test_different_config_different_hash(self):
|
|
"""Different config produces different hash."""
|
|
job1 = self._make_job({"batch_size": 50})
|
|
job2 = self._make_job({"batch_size": 100})
|
|
assert compute_config_hash(job1) != compute_config_hash(job2)
|
|
|
|
def test_source_dialect_affects_hash(self):
|
|
"""Changing source_dialect changes hash."""
|
|
job1 = self._make_job({"source_dialect": "postgresql"})
|
|
job2 = self._make_job({"source_dialect": "mysql"})
|
|
assert compute_config_hash(job1) != compute_config_hash(job2)
|
|
|
|
def test_target_languages_sorted(self):
|
|
"""target_languages are sorted before hashing."""
|
|
job1 = self._make_job({"target_languages": ["de", "ru"]})
|
|
job2 = self._make_job({"target_languages": ["ru", "de"]})
|
|
assert compute_config_hash(job1) == compute_config_hash(job2)
|
|
|
|
def test_target_languages_empty(self):
|
|
"""Empty target_languages handled."""
|
|
job = self._make_job({"target_languages": None})
|
|
h = compute_config_hash(job)
|
|
assert isinstance(h, str)
|
|
assert len(h) == 16
|
|
|
|
|
|
class TestComputeDictSnapshotHash:
|
|
"""compute_dict_snapshot_hash — dictionary state hash."""
|
|
|
|
def test_no_dictionary_links(self):
|
|
"""No dictionary links returns hash of empty bytes."""
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
h = compute_dict_snapshot_hash(db, "job-1")
|
|
assert isinstance(h, str)
|
|
assert len(h) == 16
|
|
|
|
def test_with_dictionary_links(self):
|
|
"""Dictionary links with entries produce a hash."""
|
|
db = MagicMock()
|
|
mock_link = MagicMock()
|
|
mock_link.dictionary_id = "dict-1"
|
|
db.query.return_value.filter.return_value.all.return_value = [mock_link]
|
|
db.query.return_value.filter.return_value.scalar.return_value = 5 # entry_count
|
|
db.query.return_value.filter.return_value.scalar.side_effect = [
|
|
5, # entry_count for dict-1
|
|
datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc), # max_updated
|
|
]
|
|
h = compute_dict_snapshot_hash(db, "job-1")
|
|
assert isinstance(h, str)
|
|
assert len(h) == 16
|
|
|
|
def test_deterministic_with_same_data(self):
|
|
"""Same data produces same hash."""
|
|
db1 = MagicMock()
|
|
db2 = MagicMock()
|
|
mock_link = MagicMock()
|
|
mock_link.dictionary_id = "dict-1"
|
|
db1.query.return_value.filter.return_value.all.return_value = [mock_link]
|
|
db2.query.return_value.filter.return_value.all.return_value = [mock_link]
|
|
db1.query.return_value.filter.return_value.scalar.side_effect = [5, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
|
|
db2.query.return_value.filter.return_value.scalar.side_effect = [5, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
|
|
h1 = compute_dict_snapshot_hash(db1, "job-1")
|
|
h2 = compute_dict_snapshot_hash(db2, "job-1")
|
|
assert h1 == h2
|
|
|
|
def test_different_entry_count_changes_hash(self):
|
|
"""Different entry count produces different hash."""
|
|
db1 = MagicMock()
|
|
db2 = MagicMock()
|
|
mock_link = MagicMock()
|
|
mock_link.dictionary_id = "dict-1"
|
|
db1.query.return_value.filter.return_value.all.return_value = [mock_link]
|
|
db2.query.return_value.filter.return_value.all.return_value = [mock_link]
|
|
db1.query.return_value.filter.return_value.scalar.side_effect = [5, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
|
|
db2.query.return_value.filter.return_value.scalar.side_effect = [10, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc)]
|
|
h1 = compute_dict_snapshot_hash(db1, "job-1")
|
|
h2 = compute_dict_snapshot_hash(db2, "job-1")
|
|
assert h1 != h2
|
|
|
|
def test_max_updated_none_handled(self):
|
|
"""When max_updated is None, '0' is used."""
|
|
db = MagicMock()
|
|
mock_link = MagicMock()
|
|
mock_link.dictionary_id = "dict-1"
|
|
db.query.return_value.filter.return_value.all.return_value = [mock_link]
|
|
db.query.return_value.filter.return_value.scalar.side_effect = [5, None]
|
|
h = compute_dict_snapshot_hash(db, "job-1")
|
|
assert isinstance(h, str)
|
|
assert len(h) == 16
|
|
|
|
def test_multiple_dictionaries(self):
|
|
"""Multiple dictionary links handled."""
|
|
db = MagicMock()
|
|
link1 = MagicMock()
|
|
link1.dictionary_id = "dict-a"
|
|
link2 = MagicMock()
|
|
link2.dictionary_id = "dict-b"
|
|
db.query.return_value.filter.return_value.all.return_value = [link2, link1] # unsorted
|
|
db.query.return_value.filter.return_value.scalar.side_effect = [
|
|
3, datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc), # dict-a
|
|
7, datetime(2026, 2, 1, 8, 0, 0, tzinfo=timezone.utc), # dict-b
|
|
]
|
|
h = compute_dict_snapshot_hash(db, "job-1")
|
|
assert isinstance(h, str)
|
|
assert len(h) == 16
|
|
# #endregion Test.OrchestratorConfig
|