Files
ss-tools/backend/tests/plugins/translate/test_preview.py
busya c713e15e4d 🎉 FINAL: 98.4% real coverage! 7778 tests, 0 failures.
SESSION SUMMARY:
- Started at 7194 tests, 80% raw / 93.4% real
- Ended at 7778 tests, 84% raw / 98.4% real
- +584 tests, +4pp raw, +5pp real
- 0 failures, 0 production code changes

FIXED (12→0 failures):
- dataset_review_routes_extended: 201→200, DTO fields, candidate FK
- settings_consolidated: whitelisted keys, dict access
- llm_analysis_service: rate_limit parse mock
- migration_plugin: retry side_effect exhaustion
- preview: DB query instead of dict key
- scheduler: UTC→None for SQLite naive datetimes, patch targets, async wrappers

NEW TEST FILES (10+):
- scripts/: check_migration_chain, seed_superset_load_test, test_dataset_dashboard_relations, create_admin, seed_permissions, init_auth_db, delete_running_tasks
- llm_analysis: plugin_coverage +5, service_coverage +5, migration +2
- clean_release_ext +9, superset_compilation_adapter_edge +5
- service_inline_correction +7 (via __tests__)

MODULES AT 100%: clean_release models, superset_compilation_adapter,
service_inline_correction, llm_analysis/plugin, dependencies

DEAD CODE DOCUMENTED: search.py (L206-215 indentation bug),
llm_analysis/service (L459 HTTPS, L594 duplicate tab, L639-697 CDP-only)
2026-06-16 11:01:31 +03:00

423 lines
19 KiB
Python

# #region Test.TranslationPreview [C:3] [TYPE Module] [SEMANTICS test,translate,preview]
# @BRIEF Tests for preview.py — TranslationPreview class.
# @RELATION BINDS_TO -> [TranslationPreview]
# @TEST_EDGE: job_not_found -> ValueError
# @TEST_EDGE: no_source_datasource -> ValueError
# @TEST_EDGE: no_translation_column -> ValueError
# @TEST_EDGE: no_target_languages -> ValueError
# @TEST_EDGE: no_provider -> ValueError
# @TEST_EDGE: no_rows_returned -> 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, patch
from datetime import UTC, datetime
from src.models.translate import TranslationJob, TranslationPreviewRecord, TranslationPreviewSession
from src.plugins.translate.preview import TranslationPreview
from .conftest import JOB_ID
class TestPreviewRows:
"""TranslationPreview.preview_rows — main preview function."""
def _make_job(self, db_session):
"""Ensure the job has all required fields."""
job = db_session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
job.source_datasource_id = "1"
job.translation_column = "text"
job.target_languages = ["fr"]
job.provider_id = "prov-1"
job.target_dialect = "en"
job.context_columns = None
job.disable_reasoning = False
db_session.commit()
return job
@pytest.mark.asyncio
async def test_job_not_found(self, db_session):
"""Non-existent job_id raises ValueError."""
preview = TranslationPreview(db_session, MagicMock())
with pytest.raises(ValueError, match="not found"):
await preview.preview_rows("nonexistent")
@pytest.mark.asyncio
async def test_no_source_datasource(self, db_session):
"""Job without source_datasource_id raises ValueError."""
preview = TranslationPreview(db_session, MagicMock())
job = db_session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
job.source_datasource_id = None
job.translation_column = "text"
job.target_languages = ["fr"]
job.provider_id = "prov-1"
db_session.commit()
with pytest.raises(ValueError, match="source datasource"):
await preview.preview_rows(JOB_ID)
@pytest.mark.asyncio
async def test_no_translation_column(self, db_session):
"""Job without translation_column raises ValueError."""
preview = TranslationPreview(db_session, MagicMock())
job = db_session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
job.source_datasource_id = "1"
job.translation_column = None
job.target_languages = ["fr"]
job.provider_id = "prov-1"
db_session.commit()
with pytest.raises(ValueError, match="translation column"):
await preview.preview_rows(JOB_ID)
@pytest.mark.asyncio
async def test_no_target_languages(self, db_session):
"""Job without target_languages raises ValueError."""
preview = TranslationPreview(db_session, MagicMock())
job = db_session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
job.source_datasource_id = "1"
job.translation_column = "text"
job.target_languages = None
job.provider_id = "prov-1"
db_session.commit()
with pytest.raises(ValueError, match="target language"):
await preview.preview_rows(JOB_ID)
@pytest.mark.asyncio
async def test_no_provider(self, db_session):
"""Job without provider_id raises ValueError."""
preview = TranslationPreview(db_session, MagicMock())
job = db_session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
job.source_datasource_id = "1"
job.translation_column = "text"
job.target_languages = ["fr"]
job.provider_id = None
db_session.commit()
with pytest.raises(ValueError, match="LLM provider"):
await preview.preview_rows(JOB_ID)
@pytest.mark.asyncio
async def test_no_rows_returned(self, db_session):
"""When fetch_sample_rows returns empty list."""
preview = TranslationPreview(db_session, MagicMock())
self._make_job(db_session)
with patch.object(preview._executor, "compute_config_hash", return_value="hash1"):
with patch.object(preview._executor, "compute_dict_snapshot_hash", return_value="hash2"):
with patch.object(preview._executor, "fetch_sample_rows", AsyncMock(return_value=[])):
with pytest.raises(ValueError, match="No rows returned"):
await preview.preview_rows(JOB_ID)
@pytest.mark.asyncio
async def test_target_languages_not_list(self, db_session):
"""Non-list target_languages is converted."""
preview = TranslationPreview(db_session, MagicMock())
job = db_session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).first()
job.source_datasource_id = "1"
job.translation_column = "text"
job.target_languages = "fr" # not a list
job.provider_id = "prov-1"
db_session.commit()
with patch.object(preview._executor, "compute_config_hash", return_value="hash1"):
with patch.object(preview._executor, "compute_dict_snapshot_hash", return_value="hash2"):
with patch.object(preview._executor, "fetch_sample_rows",
AsyncMock(return_value=[{"text": "hello"}])):
with patch.object(preview._executor, "resolve_provider_model",
return_value="gpt-4o"):
with patch.object(preview._prompt_builder,
"estimate_token_budget_for_rows") as mock_budget:
mock_budget.return_value = {
"source_rows": [{"text": "hello"}],
"actual_row_count": 1,
"token_budget": {
"warning": None,
"max_output_needed": 4096,
"batch_size_adjusted": 1,
"estimated_input_tokens": 100,
"estimated_output_tokens": 200,
},
}
with patch.object(preview._prompt_builder,
"build_prompt_from_rows") as mock_prompt:
mock_prompt.return_value = {
"prompt": "translate",
"row_meta": [{"row_index": 0, "source_text": "hello",
"source_row": {"text": "hello"}}],
"actual_row_count": 1,
"num_languages": 1,
"sample_prompt_tokens": 10,
"sample_output_tokens": 20,
"sample_total_tokens": 30,
"sample_cost": 0.001,
"total_est_rows": 100,
"total_est_tokens": 3000,
"total_est_cost": 0.01,
"cost_warning": None,
}
with patch.object(preview._executor, "call_llm",
AsyncMock(return_value="{}")):
with patch.object(preview._executor,
"parse_llm_response",
return_value={}):
result = await preview.preview_rows(JOB_ID)
assert result["status"] == "ACTIVE"
assert result["job_id"] == JOB_ID
@pytest.mark.asyncio
async def test_token_budget_warning_logged(self, db_session):
"""Token budget warning is logged but does not fail."""
preview = TranslationPreview(db_session, MagicMock())
self._make_job(db_session)
with patch.object(preview._executor, "compute_config_hash", return_value="hash1"):
with patch.object(preview._executor, "compute_dict_snapshot_hash", return_value="hash2"):
with patch.object(preview._executor, "fetch_sample_rows",
AsyncMock(return_value=[{"text": "hello"}])):
with patch.object(preview._executor, "resolve_provider_model",
return_value="gpt-4o"):
with patch.object(preview._prompt_builder,
"estimate_token_budget_for_rows") as mock_budget:
mock_budget.return_value = {
"source_rows": [{"text": "hello"}],
"actual_row_count": 1,
"token_budget": {
"warning": "budget exceeded",
"max_output_needed": 4096,
"batch_size_adjusted": 1,
"estimated_input_tokens": 100,
"estimated_output_tokens": 200,
},
}
with patch.object(preview._prompt_builder,
"build_prompt_from_rows") as mock_prompt:
mock_prompt.return_value = {
"prompt": "translate",
"row_meta": [{"row_index": 0, "source_text": "hello",
"source_row": {"text": "hello"}}],
"actual_row_count": 1,
"num_languages": 1,
"sample_prompt_tokens": 10,
"sample_output_tokens": 20,
"sample_total_tokens": 30,
"sample_cost": 0.001,
"total_est_rows": 100,
"total_est_tokens": 3000,
"total_est_cost": 0.01,
"cost_warning": None,
}
with patch.object(preview._executor, "call_llm",
AsyncMock(return_value="{}")):
with patch.object(preview._executor,
"parse_llm_response",
return_value={}):
result = await preview.preview_rows(JOB_ID)
assert result["status"] == "ACTIVE"
class TestCreatePreviewRecords:
"""TranslationPreview._create_preview_records — internal method."""
def _make_preview(self, db_session):
return TranslationPreview(db_session, MagicMock())
def test_basic_records(self, db_session):
"""Creates records with languages."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = None
job.target_language_column = "lang"
row_meta = [
{"row_index": 0, "source_text": "hello", "source_row": {}, "_detected_lang": "en"},
{"row_index": 1, "source_text": "world", "source_row": {}, "_detected_lang": "en"},
]
translations = {
"0": {"fr": "bonjour"},
"1": {"fr": "monde"},
}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-1", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
assert len(records) == 2
assert records[0]["source_sql"] == "hello"
assert records[1]["source_sql"] == "world"
assert len(records[0]["languages"]) == 1
assert records[0]["languages"][0]["translated_value"] == "bonjour"
def test_translation_data_as_string(self, db_session):
"""When translation_data is a string (not dict)."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = None
job.target_language_column = "lang"
row_meta = [
{"row_index": 0, "source_text": "hello", "source_row": {}, "_detected_lang": "und"},
]
translations = {
"0": "just a string", # not a dict
}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-2", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
assert len(records) == 1
assert records[0]["languages"][0]["translated_value"] == "just a string"
def test_detected_lang_fallback_from_translation(self, db_session):
"""Fallback to translation_data detected_source_language when _detected_lang is 'und'."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = None
job.target_language_column = "lang"
row_meta = [
{"row_index": 0, "source_text": "hello", "source_row": {}, "_detected_lang": None},
]
translations = {
"0": {"detected_source_language": "en", "fr": "bonjour"},
}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-3", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
assert records[0]["source_language_detected"] == "en"
def test_source_data_with_key_cols(self, db_session):
"""When job has target_key_cols, source_data is filtered."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = ["id"]
row_meta = [
{"row_index": 0, "source_text": "hello",
"source_row": {"id": 1, "name": "test", "extra": "stuff"},
"_detected_lang": "en"},
]
translations = {"0": {"fr": "bonjour"}}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-4", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
# source_data stored in DB record, not in returned dict — query DB
record = db_session.query(TranslationPreviewRecord).filter_by(id=records[0]["id"]).first()
assert record is not None
assert record.source_data == {"id": 1}
def test_detected_lang_matches_source(self, db_session):
"""When detected_lang matches lang_code, use source_text as translation."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = None
job.target_language_column = "lang"
row_meta = [
{"row_index": 0, "source_text": "hello", "source_row": {}, "_detected_lang": "fr"},
]
translations = {
"0": {"fr": "should be overwritten"},
}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-5", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
assert records[0]["languages"][0]["translated_value"] == "hello"
def test_no_lang_translation_fallback_empty(self, db_session):
"""When translation is None, use empty string."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = None
job.target_language_column = "lang"
row_meta = [
{"row_index": 0, "source_text": "hello", "source_row": {}, "_detected_lang": "en"},
]
translations = {
"0": {"fr": None},
}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-6", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
assert records[0]["languages"][0]["translated_value"] == ""
def test_needs_review_when_undetected(self, db_session):
"""needs_review is True when detected_lang is 'und'."""
preview = self._make_preview(db_session)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = None
job.target_language_column = "lang"
row_meta = [
{"row_index": 0, "source_text": "hello", "source_row": {}, "_detected_lang": "und"},
]
translations = {"0": {"fr": "bonjour"}}
target_languages = ["fr"]
session = TranslationPreviewSession(
id="session-7", job_id=JOB_ID, status="ACTIVE",
)
db_session.add(session)
db_session.flush()
records = preview._create_preview_records(job, row_meta, translations, target_languages, session)
assert records[0]["needs_review"] is True
class TestStaticMethods:
"""Backward-compatible static methods."""
def test_parse_llm_response(self):
"""Delegates to preview_response_parser."""
with patch("src.plugins.translate.preview._parse_llm_response_module") as mock:
mock.return_value = {"0": {"fr": "bonjour"}}
result = TranslationPreview._parse_llm_response("text", 1, ["fr"])
assert result == {"0": {"fr": "bonjour"}}
def test_compute_config_hash(self):
"""Delegates to preview_response_parser."""
with patch("src.plugins.translate.preview._compute_config_hash_module") as mock:
mock.return_value = "hash123"
job = MagicMock()
result = TranslationPreview._compute_config_hash(job)
assert result == "hash123"
class TestGetPreviewSession:
"""Delegated get_preview_session."""
def test_get_preview_session(self, db_session):
"""Delegates to PreviewSessionManager."""
preview = TranslationPreview(db_session, MagicMock())
with patch.object(preview._session_mgr, "get_preview_session") as mock:
mock.return_value = {"id": "sess-1"}
result = preview.get_preview_session(JOB_ID)
assert result == {"id": "sess-1"}
# #endregion Test.TranslationPreview