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