Files
ss-tools/backend/tests/plugins/translate/test_preview_executor.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +03:00

362 lines
16 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 -> [Plugin.PreviewExecutor]
# @TEST_EDGE: no_env_config -> ValueError
# @TEST_EDGE: fetch_failure -> ValueError
# @TEST_EDGE: no_provider_id -> ValueError
# @TEST_EDGE: unsupported_provider -> ValueError
from pathlib import Path
import pytest
import sys
from unittest.mock import AsyncMock, MagicMock, patch
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
from src.models.translate import TranslationJob
from src.plugins.translate.preview_executor import PreviewExecutor
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"
# #region Test.PreviewExecutor.TestInvalidJsonRetry [C:2] [TYPE Function]
# @BRIEF Verify malformed provider JSON gets the same bounded preview retry as empty content.
# @TEST_EDGE: invalid_provider_json -> retry with doubled max_tokens
@pytest.mark.asyncio
async def test_invalid_json_retry(self, db_session):
"""Malformed provider JSON is retried once before the preview fails."""
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("LLM provider returned an invalid JSON response"),
("retried content", "stop"),
]
),
) as mock_call:
result = await executor.call_llm(job, "translate this", 4096)
assert result == "retried content"
assert mock_call.call_count == 2
assert mock_call.call_args_list[0].kwargs["max_tokens"] == 4096
assert mock_call.call_args_list[1].kwargs["max_tokens"] == 8192
# #endregion Test.PreviewExecutor.TestInvalidJsonRetry
@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