Files
ss-tools/backend/tests/plugins/translate/test_preview.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

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, 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