Files
ss-tools/backend/tests/plugins/translate/test_preview_prompt_builder.py

341 lines
14 KiB
Python

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