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

339 lines
13 KiB
Python

# #region Test.BulkFindReplaceService [C:3] [TYPE Module] [SEMANTICS test,bulk,find,replace]
# @BRIEF Tests for service_bulk_replace.py — BulkFindReplaceService.
# @RELATION BINDS_TO -> [BulkFindReplaceService]
# @TEST_EDGE: pattern_too_long -> ValueError
# @TEST_EDGE: invalid_regex -> ValueError
# @TEST_EDGE: no_match -> empty preview
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import re
import pytest
from unittest.mock import MagicMock, patch
from datetime import UTC, datetime
from src.models.translate import (
TranslationBatch, TranslationLanguage, TranslationRecord, TranslationRun,
)
from src.plugins.translate.service_bulk_replace import BulkFindReplaceService
from .conftest import JOB_ID
def _make_batch_and_run(db_session, run_id="run-br"):
"""Create a batch and run for FK constraints."""
now = datetime.now(UTC)
run = TranslationRun(id=run_id, job_id=JOB_ID, status="RUNNING",
started_at=now)
db_session.add(run)
db_session.flush()
batch = TranslationBatch(id=f"batch-{run_id}", run_id=run_id,
batch_index=0, status="PENDING")
db_session.add(batch)
db_session.flush()
return run_id, batch.id
class TestCompilePattern:
"""BulkFindReplaceService._compile_pattern."""
def test_plain_text(self):
"""Plain text pattern is escaped."""
pattern = BulkFindReplaceService._compile_pattern("hello (world)", False)
assert isinstance(pattern, re.Pattern)
assert pattern.search("hello (world)")
assert not pattern.search("hello world")
def test_regex(self):
"""Regex pattern is compiled as-is."""
pattern = BulkFindReplaceService._compile_pattern(r"hello\s+world", True)
assert isinstance(pattern, re.Pattern)
assert pattern.search("hello world")
assert not pattern.search("helloworld")
def test_too_long(self):
"""Pattern exceeding max length raises ValueError."""
with pytest.raises(ValueError, match="Pattern too long"):
BulkFindReplaceService._compile_pattern("x" * 501, False)
def test_invalid_regex(self):
"""Invalid regex raises ValueError."""
with pytest.raises(ValueError, match="Invalid regex"):
BulkFindReplaceService._compile_pattern(r"[invalid", True)
class TestFindMatchingEntries:
"""BulkFindReplaceService._find_matching_entries."""
def test_returns_entries(self, db_session):
"""Returns entries matching run and language."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-1")
record = TranslationRecord(
id="rec-br-1", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-1", language_code="fr",
translated_value="bonjour", final_value="bonjour_edited",
status="edited",
)
db_session.add(lang)
db_session.commit()
entries = BulkFindReplaceService._find_matching_entries(
db_session, run_id, "hello", False, "fr"
)
assert len(entries) == 1
assert entries[0].language_code == "fr"
def test_filters_by_language(self, db_session):
"""Only entries with matching language_code returned."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-2")
record = TranslationRecord(
id="rec-br-2", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang_fr = TranslationLanguage(
record_id="rec-br-2", language_code="fr",
translated_value="bonjour", final_value="bonjour",
status="translated",
)
lang_de = TranslationLanguage(
record_id="rec-br-2", language_code="de",
translated_value="hallo", final_value="hallo",
status="translated",
)
db_session.add(lang_fr)
db_session.add(lang_de)
db_session.commit()
entries = BulkFindReplaceService._find_matching_entries(
db_session, run_id, "hello", False, "fr"
)
assert len(entries) == 1
assert entries[0].language_code == "fr"
class TestPreview:
"""BulkFindReplaceService.preview."""
def test_preview_finds_match(self, db_session):
"""Returns preview items with matched entries."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-prev")
record = TranslationRecord(
id="rec-br-3", run_id=run_id, batch_id=batch_id,
source_sql="hello world", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-3", language_code="fr",
translated_value="bonjour le monde", final_value="bonjour le monde",
status="translated",
)
db_session.add(lang)
db_session.commit()
results = BulkFindReplaceService.preview(
db_session, run_id, "monde", False, "terre", "fr"
)
assert len(results) == 1
assert results[0]["new_value"] == "bonjour le terre"
def test_preview_no_match(self, db_session):
"""No matching entries returns empty list."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-no")
record = TranslationRecord(
id="rec-br-4", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-4", language_code="fr",
translated_value="bonjour", final_value="bonjour",
status="translated",
)
db_session.add(lang)
db_session.commit()
results = BulkFindReplaceService.preview(
db_session, run_id, "zzzzz", False, "dummy", "fr"
)
assert len(results) == 0
class TestApply:
"""BulkFindReplaceService.apply."""
def test_apply_basic(self, db_session):
"""Apply replacement and verify DB updated."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-apply-1")
record = TranslationRecord(
id="rec-br-apply-1", run_id=run_id, batch_id=batch_id,
source_sql="hello world", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-apply-1", language_code="fr",
translated_value="bonjour le monde", final_value="bonjour le monde",
status="translated",
)
db_session.add(lang)
db_session.commit()
result = BulkFindReplaceService.apply(
db_session, run_id, "monde", False, "terre", "fr"
)
assert result["rows_affected"] == 1
db_session.refresh(lang)
assert lang.final_value == "bonjour le terre"
def test_apply_no_match(self, db_session):
"""No match results in 0 rows affected."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-apply-2")
record = TranslationRecord(
id="rec-br-apply-2", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-apply-2", language_code="fr",
translated_value="bonjour", final_value="bonjour",
status="translated",
)
db_session.add(lang)
db_session.commit()
result = BulkFindReplaceService.apply(
db_session, run_id, "zzzzz", False, "terre", "fr"
)
assert result["rows_affected"] == 0
def test_apply_with_dictionary_submit(self, db_session):
"""Apply with dictionary submission."""
from src.models.translate import TerminologyDictionary
dict_entry = TerminologyDictionary(id="dict-br-1", name="Test Dict", is_active=True)
db_session.add(dict_entry)
db_session.commit()
run_id, batch_id = _make_batch_and_run(db_session, "run-br-apply-3")
record = TranslationRecord(
id="rec-br-apply-3", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-apply-3", language_code="fr",
translated_value="bonjour tout le monde", final_value="bonjour tout le monde",
status="translated",
)
db_session.add(lang)
db_session.commit()
with patch("src.plugins.translate.service_bulk_replace.InlineCorrectionService.submit_correction_to_dict") as mock_submit:
mock_submit.return_value = {"action": "created", "entry_id": "entry-1"}
result = BulkFindReplaceService.apply(
db_session, run_id, "monde", False, "terre",
"fr", submit_to_dictionary=True, dictionary_id="dict-br-1",
)
assert result["rows_affected"] == 1
assert result["corrections_submitted"] == 1
def test_apply_with_dictionary_submit_with_context(self, db_session):
"""Apply with dictionary submission with context."""
from src.models.translate import TerminologyDictionary
dict_entry = TerminologyDictionary(id="dict-br-ctx", name="Test Dict", is_active=True)
db_session.add(dict_entry)
db_session.commit()
run_id, batch_id = _make_batch_and_run(db_session, "run-br-apply-ctx")
record = TranslationRecord(
id="rec-br-apply-ctx", run_id=run_id, batch_id=batch_id,
source_sql="hello world", target_sql="bonjour", status="SUCCESS",
source_data={"table": "test"}, source_object_type="table_row",
source_object_name="Row 1", source_object_id="1",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-apply-ctx", language_code="fr",
translated_value="bonjour le monde", final_value="bonjour le monde",
status="translated",
)
db_session.add(lang)
db_session.commit()
with patch("src.plugins.translate.service_bulk_replace.InlineCorrectionService.submit_correction_to_dict") as mock_submit:
mock_submit.return_value = {"action": "created", "entry_id": "entry-ctx"}
result = BulkFindReplaceService.apply(
db_session, run_id, "monde", False, "terre",
"fr", submit_to_dictionary=True, dictionary_id="dict-br-ctx",
submit_to_dictionary_with_context=True,
)
assert result["rows_affected"] == 1
assert result["corrections_submitted"] == 1
def test_apply_dictionary_submit_raises_exception(self, db_session):
"""Exception during dictionary submit caught gracefully."""
from src.models.translate import TerminologyDictionary
dict_entry = TerminologyDictionary(id="dict-br-exc", name="Test Dict", is_active=True)
db_session.add(dict_entry)
db_session.commit()
run_id, batch_id = _make_batch_and_run(db_session, "run-br-apply-exc")
record = TranslationRecord(
id="rec-br-apply-exc", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-apply-exc", language_code="fr",
translated_value="bonjour", final_value="bonjour",
status="translated",
)
db_session.add(lang)
db_session.commit()
with patch("src.plugins.translate.service_bulk_replace.InlineCorrectionService.submit_correction_to_dict") as mock_submit:
mock_submit.side_effect = Exception("Dict failure")
result = BulkFindReplaceService.apply(
db_session, run_id, "bonjour", False, "salut",
"fr", submit_to_dictionary=True, dictionary_id="dict-br-exc",
)
assert result["corrections_submitted"] == 0
assert result["rows_affected"] == 1
def test_apply_pending_status_becomes_edited(self, db_session):
"""Entry with 'pending' status becomes 'edited'."""
run_id, batch_id = _make_batch_and_run(db_session, "run-br-status")
record = TranslationRecord(
id="rec-br-status", run_id=run_id, batch_id=batch_id,
source_sql="hello", target_sql="bonjour", status="SUCCESS",
)
db_session.add(record)
db_session.flush()
lang = TranslationLanguage(
record_id="rec-br-status", language_code="fr",
translated_value="bonjour", final_value="bonjour",
status="pending",
)
db_session.add(lang)
db_session.commit()
BulkFindReplaceService.apply(
db_session, run_id, "bonjour", False, "salut", "fr"
)
db_session.refresh(lang)
assert lang.status == "edited"
# #endregion Test.BulkFindReplaceService