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.
331 lines
14 KiB
Python
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
|