# #region Test.PreviewPromptBuilder [C:3] [TYPE Module] [SEMANTICS test, translate, preview, prompt, builder] # @BRIEF Tests for preview_prompt_builder.py — PreviewPromptBuilder.build_prompt_from_rows. # @RELATION BINDS_TO -> [PreviewPromptBuilder] # @TEST_CONTRACT: build_prompt_from_rows -> dict | builds LLM prompt with glossary # @TEST_CONTRACT: estimate_token_budget_for_rows -> dict | delegates to helper # @TEST_EDGE: dictionary_matches # @TEST_EDGE: no_dictionary_matches # @TEST_EDGE: custom_prompt_template import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) import os os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing") os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") from unittest.mock import MagicMock, patch import pytest from src.plugins.translate.preview_prompt_builder import PreviewPromptBuilder class TestBuildPromptFromRows: """PreviewPromptBuilder.build_prompt_from_rows — builds prompt with dictionary glossary.""" def _make_job(self, **kwargs): job = MagicMock() job.id = "job-1" job.source_dialect = "postgresql" job.target_dialect = "clickhouse" job.target_languages = ["ru", "de"] job.translation_column = "name" job.context_columns = ["description"] for k, v in kwargs.items(): setattr(job, k, v) return job def _make_db(self): return MagicMock() def test_build_prompt_basic(self): """Builds prompt with row data and no dictionary section.""" db = self._make_db() job = self._make_job() source_rows = [ {"name": "hello", "description": "greeting"}, {"name": "world", "description": "earth"}, ] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "rendered prompt" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = { "prompt": "rendered prompt", "row_meta": [], "target_languages": ["ru", "de"], "num_languages": 2, "dictionary_section": "", "sample_prompt_tokens": 100, "sample_output_tokens": 200, "sample_total_tokens": 300, "sample_cost": 0.001, "total_est_rows": 50, "total_est_tokens": 500, "total_est_cost": 0.01, "cost_warning": None, "actual_row_count": 2, } builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=5) assert result["prompt"] == "rendered prompt" assert result["sample_prompt_tokens"] == 100 mock_filter.assert_called_once() mock_render.assert_called_once() def test_build_prompt_with_dictionary(self): """Dictionary matches produce glossary section.""" db = self._make_db() job = self._make_job() source_rows = [{"name": "hello", "description": "greeting"}] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [ {"source_term": "hello", "target_term": "привет", "context_notes": "informal"}, {"source_term": "world", "target_term": "мир", "context_notes": None}, ] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.side_effect = lambda template, ctx: ctx.get("dictionary_section", "") with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = { "prompt": "", "row_meta": [], "target_languages": ["ru", "de"], "num_languages": 2, "dictionary_section": "...", "sample_prompt_tokens": 0, "sample_output_tokens": 0, "sample_total_tokens": 0, "sample_cost": 0.0, "total_est_rows": 10, "total_est_tokens": 0, "total_est_cost": 0.0, "cost_warning": None, "actual_row_count": 1, } builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=1) # Verify dictionary was included in render mock_render.assert_called_once() variables = mock_render.call_args[0][1] assert "Terminology dictionary" in variables["dictionary_section"] assert "hello" in variables["dictionary_section"] assert "привет" in variables["dictionary_section"] assert "informal" in variables["dictionary_section"] def test_build_prompt_no_context_columns(self): """Job without context columns still works.""" db = self._make_db() job = self._make_job(context_columns=None) source_rows = [{"name": "hello"}] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "prompt" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "prompt"} builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=1) assert result["prompt"] == "prompt" def test_custom_prompt_template(self): """Custom prompt template overrides default.""" db = self._make_db() job = self._make_job() source_rows = [{"name": "hello"}] custom_template = "Custom: {source_language} -> {target_languages}" with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "custom rendered" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "custom rendered"} builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows( job, source_rows, sample_size=1, prompt_template=custom_template, ) # Verify custom template was used mock_render.assert_called_once() assert mock_render.call_args[0][0] == custom_template def test_default_prompt_template(self): """Default prompt template when none provided.""" db = self._make_db() job = self._make_job() source_rows = [{"name": "hello"}] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "default rendered" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "default rendered"} from src.plugins.translate.preview_constants import DEFAULT_PREVIEW_PROMPT_TEMPLATE builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=1) assert mock_render.call_args[0][0] == DEFAULT_PREVIEW_PROMPT_TEMPLATE def test_string_target_languages_converted(self): """String target_languages converted to list.""" db = self._make_db() job = self._make_job(target_languages="ru") # string, not list source_rows = [{"name": "hello"}] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "prompt" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "prompt"} builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=1) # Verify target_languages was converted to list mock_render.assert_called_once() variables = mock_render.call_args[0][1] assert "ru" in variables["target_languages"] def test_empty_source_rows(self): """Empty source rows handled.""" db = self._make_db() job = self._make_job() source_rows = [] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "prompt" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "prompt"} builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=5) assert result["prompt"] == "prompt" def test_json_rows_format(self): """Rows are serialized to JSON in prompt context.""" db = self._make_db() job = self._make_job() source_rows = [{"name": "hello", "description": "greeting"}] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "prompt" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "prompt"} builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=1) variables = mock_render.call_args[0][1] assert "row_id" in variables["rows_json"] assert "text" in variables["rows_json"] assert "hello" in variables["rows_json"] def test_row_meta_contains_context(self): """Row meta includes context_data from context columns.""" db = self._make_db() job = self._make_job(context_columns=["description", "category"]) source_rows = [{"name": "hello", "description": "greeting", "category": "common"}] with patch( "src.plugins.translate.preview_prompt_builder.DictionaryManager.filter_for_batch" ) as mock_filter: mock_filter.return_value = [] with patch( "src.plugins.translate.preview_prompt_builder.render_prompt" ) as mock_render: mock_render.return_value = "prompt" with patch( "src.plugins.translate.preview_prompt_builder.compute_build_token_metadata" ) as mock_meta: mock_meta.return_value = {"prompt": "prompt"} builder = PreviewPromptBuilder(db) result = builder.build_prompt_from_rows(job, source_rows, sample_size=1) # Verify the row_context was passed to filter_for_batch mock_filter.assert_called_once() filter_kwargs = mock_filter.call_args[1] assert filter_kwargs["row_context"] == {"description": "greeting", "category": "common"} class TestEstimateTokenBudgetForRows: """PreviewPromptBuilder.estimate_token_budget_for_rows — delegates to helper.""" def test_delegates_to_helper(self): """Calls estimate_token_budget_for_rows helper.""" db = MagicMock() job = MagicMock() source_rows = [{"name": "hello"}] with patch( "src.plugins.translate.preview_prompt_builder.estimate_token_budget_for_rows" ) as mock_helper: mock_helper.return_value = {"actual_row_count": 1, "source_rows": source_rows} builder = PreviewPromptBuilder(db) result = builder.estimate_token_budget_for_rows( source_rows, ["ru"], job, "gpt-4o-mini", ) mock_helper.assert_called_once_with( source_rows=source_rows, target_languages=["ru"], job=job, provider_model="gpt-4o-mini", ) assert result == {"actual_row_count": 1, "source_rows": source_rows} # #endregion Test.PreviewPromptBuilder