Files
ss-tools/backend/tests/plugins/translate/test_preview_executor.py
busya 8a4310169b v4: 7643 tests passing, 83% raw / 96.9% real coverage (excl __tests__).
12 known failures — all from Agent 3 new unverified tests (mock setup issues):
- 3 dataset_review_routes_extended (DTO field mismatches)
- 1 settings_consolidated (dict key access)
- 1 llm_analysis_service_coverage (rate_limit mock)
- 1 migration_plugin (SessionLocal side_effect exhaustion)
- 1 preview (DB query vs dict key)
- 5 scheduler (datetime timezone + async mock mismatches)

NEW TEST FILES THIS SESSION:
- test_batch_insert_coverage.py — 3 tests
- test_storage_plugin.py — +3 tests
- test_search.py — +2 tests
- test_mapper.py — already 100%
- test_llm_analysis_migration_v1_to_v2.py — 14 tests
- test_llm_async_http.py — +1 test
- test_prompt_builder.py — +1 test
- test_service_datasource.py — +1 test
- test_lang_detect.py — +1 test
- test_scheduler.py — +6 tests
- test_llm_analysis_service_coverage.py — +15 tests
- test_dataset_review_routes_extended.py — +14 tests
- test_settings_consolidated.py — +13 tests

Modules pushed to 100%: _batch_insert, dictionary_entries, service_datasource,
_llm_async_http, prompt_builder, dictionary_crud, _batch_sizer, storage/plugin,
mapper.py

Session: 7194→7643 tests (+449), 80%→83% raw (+3pp), 93.4%→96.9% real (+3.5pp).
Remaining: 12 failures to fix + ~300 statements to reach 98% real coverage.
2026-06-16 09:34:10 +03:00

331 lines
14 KiB
Python

# #region Test.PreviewExecutor [C:3] [TYPE Module] [SEMANTICS test,preview,executor,llm]
# @BRIEF Tests for preview_executor.py — PreviewExecutor class.
# @RELATION BINDS_TO -> [PreviewExecutor]
# @TEST_EDGE: no_env_config -> ValueError
# @TEST_EDGE: fetch_failure -> ValueError
# @TEST_EDGE: no_provider_id -> ValueError
# @TEST_EDGE: unsupported_provider -> ValueError
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import pytest
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from src.models.translate import TranslationJob
from src.plugins.translate.preview_executor import PreviewExecutor
from .conftest import JOB_ID
class TestFetchSampleRows:
"""PreviewExecutor.fetch_sample_rows — fetch from Superset."""
def _make_job(self):
job = MagicMock(spec=TranslationJob)
job.source_datasource_id = "1"
job.environment_id = "env-1"
job.source_dialect = None
return job
@pytest.mark.asyncio
async def test_success(self, db_session):
"""Successful fetch returns extracted rows."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
job.environment_id = None
job.source_dialect = None
env_config = MagicMock()
env_config.id = "env-1"
config_manager = MagicMock()
config_manager.get_environments.return_value = [env_config]
executor.config_manager = config_manager
with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
mock_client = MagicMock()
mock_get.return_value = mock_client
mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
mock_client.build_dataset_preview_query_context = MagicMock(return_value={
"queries": [{}],
"form_data": {},
})
mock_client.network.request = AsyncMock()
mock_client.network.request.return_value = {"result": []}
with patch("src.plugins.translate.preview_executor._extract_data_rows",
return_value=[{"text": "hello"}]):
rows = await executor.fetch_sample_rows(job)
assert len(rows) == 1
@pytest.mark.asyncio
async def test_no_env_config_fallback(self, db_session):
"""Falls back to first env config when env_id not found."""
env_config = MagicMock()
env_config.id = "env-other"
config_manager = MagicMock()
config_manager.get_environments.return_value = [env_config]
executor = PreviewExecutor(db_session, config_manager)
job = self._make_job()
job.environment_id = "env-nonexistent"
job.source_dialect = None
with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
mock_client = MagicMock()
mock_get.return_value = mock_client
mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
mock_client.build_dataset_preview_query_context = MagicMock(return_value={
"queries": [{}],
"form_data": {},
})
mock_client.network.request = AsyncMock()
mock_client.network.request.return_value = {"result": []}
with patch("src.plugins.translate.preview_executor._extract_data_rows",
return_value=[{"text": "hello"}]):
rows = await executor.fetch_sample_rows(job)
assert len(rows) == 1
@pytest.mark.asyncio
async def test_no_environments_raises(self, db_session):
"""No environments configured raises ValueError."""
config_manager = MagicMock()
config_manager.get_environments.return_value = []
executor = PreviewExecutor(db_session, config_manager)
job = self._make_job()
with pytest.raises(ValueError, match="No Superset environments"):
await executor.fetch_sample_rows(job)
@pytest.mark.asyncio
async def test_network_error_raises(self, db_session):
"""Network error raises ValueError."""
env_config = MagicMock()
env_config.id = "env-1"
config_manager = MagicMock()
config_manager.get_environments.return_value = [env_config]
executor = PreviewExecutor(db_session, config_manager)
job = self._make_job()
with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
mock_client = MagicMock()
mock_get.return_value = mock_client
mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
mock_client.build_dataset_preview_query_context = MagicMock(return_value={"queries": [{}], "form_data": {}})
mock_client.network.request = AsyncMock(side_effect=Exception("Network error"))
with pytest.raises(ValueError, match="Failed to fetch sample data"):
await executor.fetch_sample_rows(job)
@pytest.mark.asyncio
async def test_query_context_structure(self, db_session):
"""Verify query context modifications (row_limit, result_type, etc.)."""
env_config = MagicMock()
env_config.id = "env-1"
config_manager = MagicMock()
config_manager.get_environments.return_value = [env_config]
executor = PreviewExecutor(db_session, config_manager)
job = self._make_job()
with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
mock_client = MagicMock()
mock_get.return_value = mock_client
mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
mock_client.build_dataset_preview_query_context = MagicMock(return_value={
"queries": [{"result_type": "full", "columns": ["col1"], "metrics": ["count"]}],
"form_data": {"query_mode": "aggregate"},
})
mock_client.network.request = AsyncMock()
mock_client.network.request.return_value = {"result": []}
with patch("src.plugins.translate.preview_executor._extract_data_rows",
return_value=[{"text": "hello"}]):
rows = await executor.fetch_sample_rows(job, sample_size=5)
assert len(rows) == 1
# Verify query context was modified
call_kwargs = mock_client.network.request.call_args[1]
posted_data = call_kwargs["data"]
import json
parsed = json.loads(posted_data)
assert parsed["queries"][0]["row_limit"] == 5
assert "result_type" not in parsed["queries"][0]
assert "columns" not in parsed["queries"][0]
assert parsed["queries"][0]["metrics"] == []
assert parsed["result_type"] == "samples"
assert "query_mode" not in parsed["form_data"]
class TestCallLLM:
"""PreviewExecutor.call_llm — Call LLM provider."""
def _make_job(self):
job = MagicMock(spec=TranslationJob)
job.provider_id = "prov-1"
job.disable_reasoning = False
return job
def _make_provider(self, base_url="https://api.openai.com", provider_type="openai",
default_model="gpt-4o-mini"):
provider = MagicMock()
provider.base_url = base_url
provider.provider_type = provider_type
provider.default_model = default_model
return provider
@pytest.mark.asyncio
async def test_success(self, db_session):
"""Successful LLM call returns content."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider.return_value = self._make_provider()
mock_instance.get_decrypted_api_key.return_value = "sk-test"
with patch("src.plugins.translate.preview_executor.call_openai_compatible",
AsyncMock(return_value=("translated text", "stop"))):
result = await executor.call_llm(job, "translate this", 4096)
assert result == "translated text"
@pytest.mark.asyncio
async def test_no_provider_id(self, db_session):
"""No provider_id raises ValueError."""
executor = PreviewExecutor(db_session, MagicMock())
job = MagicMock(spec=TranslationJob)
job.provider_id = None
with pytest.raises(ValueError, match="no LLM provider"):
await executor.call_llm(job, "text")
@pytest.mark.asyncio
async def test_provider_not_found(self, db_session):
"""Provider not found in DB raises ValueError."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider.return_value = None
with pytest.raises(ValueError, match="not found"):
await executor.call_llm(job, "text")
@pytest.mark.asyncio
async def test_no_api_key(self, db_session):
"""No decrypted API key raises ValueError."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider.return_value = self._make_provider()
mock_instance.get_decrypted_api_key.return_value = None
with pytest.raises(ValueError, match="Could not decrypt API key"):
await executor.call_llm(job, "text")
@pytest.mark.asyncio
async def test_unsupported_provider_type(self, db_session):
"""Unsupported provider type raises ValueError."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider.return_value = self._make_provider(
provider_type="anthropic"
)
mock_instance.get_decrypted_api_key.return_value = "sk-test"
with pytest.raises(ValueError, match="Unsupported provider type"):
await executor.call_llm(job, "text")
@pytest.mark.asyncio
async def test_empty_content_retry(self, db_session):
"""Empty content triggers retry with doubled max_tokens."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider.return_value = self._make_provider()
mock_instance.get_decrypted_api_key.return_value = "sk-test"
# First call raises ValueError("empty content"), second succeeds
with patch("src.plugins.translate.preview_executor.call_openai_compatible",
AsyncMock(side_effect=[
ValueError("empty content"),
("retried content", "stop"),
])):
result = await executor.call_llm(job, "translate this", 4096)
assert result == "retried content"
@pytest.mark.asyncio
async def test_empty_content_retry_exhausted(self, db_session):
"""If retry also returns empty content, raises."""
executor = PreviewExecutor(db_session, MagicMock())
job = self._make_job()
with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider.return_value = self._make_provider()
mock_instance.get_decrypted_api_key.return_value = "sk-test"
with patch("src.plugins.translate.preview_executor.call_openai_compatible",
AsyncMock(side_effect=[ValueError("empty content"), ValueError("another error")])):
with pytest.raises(ValueError, match="another error"):
await executor.call_llm(job, "text")
class TestDelegationHelpers:
"""PreviewExecutor delegation to preview_response_parser."""
def test_parse_llm_response(self, db_session):
"""Delegates to module function."""
executor = PreviewExecutor(db_session, MagicMock())
with patch("src.plugins.translate.preview_executor._parse_llm_response") as mock:
mock.return_value = {"0": {"fr": "bonjour"}}
result = executor.parse_llm_response("text", 1, ["fr"])
assert result == {"0": {"fr": "bonjour"}}
def test_resolve_provider_model(self, db_session):
"""Delegates to module function."""
executor = PreviewExecutor(db_session, MagicMock())
job = MagicMock()
with patch("src.plugins.translate.preview_executor._resolve_provider_model") as mock:
mock.return_value = "gpt-4o"
result = executor.resolve_provider_model(job)
assert result == "gpt-4o"
def test_compute_config_hash(self, db_session):
"""Delegates to module function."""
with patch("src.plugins.translate.preview_executor._compute_config_hash") as mock:
mock.return_value = "hash123"
job = MagicMock()
result = PreviewExecutor.compute_config_hash(job)
assert result == "hash123"
def test_compute_dict_snapshot_hash(self, db_session):
"""Delegates to module function."""
executor = PreviewExecutor(db_session, MagicMock())
with patch("src.plugins.translate.preview_executor._compute_dict_snapshot_hash") as mock:
mock.return_value = "dict-hash"
result = executor.compute_dict_snapshot_hash("job-1")
assert result == "dict-hash"
# #endregion Test.PreviewExecutor