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