341 lines
14 KiB
Python
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
|