# #region Test.RunExecutionService [C:3] [TYPE Module] [SEMANTICS test, translate, run, execution] # @BRIEF Tests for _run_service.py — RunExecutionService. # @RELATION BINDS_TO -> [_run_service] # @TEST_EDGE: filter_new_keys_no_prev_run -> all keys treated as new # @TEST_EDGE: filter_new_keys_no_key_cols -> all rows pass through # @TEST_EDGE: load_preview_edits -> edits loaded from session import pytest from unittest.mock import MagicMock, patch from datetime import UTC, datetime import uuid from sqlalchemy import create_engine, event from sqlalchemy.orm import Session, sessionmaker from src.models.translate import ( Base, TranslationBatch, TranslationJob, TranslationRun, TranslationRecord, TranslationLanguage, TranslationRunLanguageStats, TranslationPreviewSession, TranslationPreviewRecord, ) def make_session(): engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(bind=engine) session = sessionmaker(bind=engine)() return session, engine class TestRunExecutionService: """RunExecutionService — orchestrate full translation run.""" def _make_service(self, db, **kwargs): from src.plugins.translate._run_service import RunExecutionService config_manager = kwargs.pop("config_manager", MagicMock()) return RunExecutionService(db, config_manager, **kwargs) # -- _fetch_source_rows / _extract_chart_data_rows (thin wrappers) -- @pytest.mark.asyncio async def test_fetch_source_rows_calls_imported(self): """_fetch_source_rows delegates to module-level fetch_source_rows.""" session, engine = make_session() try: svc = self._make_service(session) from src.models.translate import TranslationJob job = TranslationJob(id="fsr-job", name="fsr", status="ACTIVE", source_dialect="en", target_dialect="fr") session.add(job) session.commit() with patch("src.plugins.translate._run_service.fetch_source_rows", return_value=[{"id": 1}]) as mock_fetch: result = await svc._fetch_source_rows("fsr-job", "run-fsr-1") assert result == [{"id": 1}] mock_fetch.assert_called_once_with( session, svc.config_manager, "fsr-job", "run-fsr-1" ) finally: session.close() Base.metadata.drop_all(bind=engine) def test_extract_chart_data_rows_delegates(self): """Static _extract_chart_data_rows delegates to module-level function.""" from src.plugins.translate._run_service import RunExecutionService with patch("src.plugins.translate._run_service._extract_chart_data_rows", return_value=[{"id": 1}]) as mock_extract: result = RunExecutionService._extract_chart_data_rows({"data": []}) assert result == [{"id": 1}] mock_extract.assert_called_once_with({"data": []}) # -- _filter_new_keys -- def test_filter_new_keys_no_prev_run(self): """No prior completed run -> all source rows treated as new.""" session, engine = make_session() try: job = TranslationJob(id="j1", name="j1", status="ACTIVE", source_dialect="en", target_dialect="fr") session.add(job) session.commit() svc = self._make_service(session) source_rows = [{"source_data": {"id": 1}}] result = svc._filter_new_keys(job, "run-1", source_rows) assert len(result) == 1 finally: session.close() Base.metadata.drop_all(bind=engine) def test_filter_new_keys_with_prev_run_filters(self): """Prior completed run -> rows with existing keys filtered out.""" session, engine = make_session() try: job = TranslationJob(id="j2", name="j2", status="ACTIVE", source_dialect="en", target_dialect="fr", target_key_cols=["id"]) session.add(job) prev_run = TranslationRun(id="prev-run", job_id="j2", status="COMPLETED", insert_status="succeeded", started_at=datetime.now(UTC), created_at=datetime.now(UTC)) session.add(prev_run) session.flush() batch = TranslationBatch(id="batch-prev", run_id="prev-run", batch_index=0, status="COMPLETED", created_at=datetime.now(UTC)) session.add(batch) session.flush() # Record with existing key rec = TranslationRecord(id="rec-1", run_id="prev-run", batch_id="batch-prev", status="SUCCESS", source_data={"id": 1}) session.add(rec) session.commit() svc = self._make_service(session) source_rows = [ {"source_data": {"id": 1}}, # already exists {"source_data": {"id": 2}}, # new ] result = svc._filter_new_keys(job, "curr-run", source_rows) assert len(result) == 1 assert result[0]["source_data"]["id"] == 2 finally: session.close() Base.metadata.drop_all(bind=engine) def test_filter_new_keys_no_key_cols(self): """No key cols configured -> all rows pass through.""" session, engine = make_session() try: job = TranslationJob(id="j3", name="j3", status="ACTIVE", source_dialect="en", target_dialect="fr", target_key_cols=None, source_key_cols=None) session.add(job) prev_run = TranslationRun(id="prev-run-3", job_id="j3", status="COMPLETED", insert_status="succeeded", started_at=datetime.now(UTC), created_at=datetime.now(UTC)) session.add(prev_run) session.commit() svc = self._make_service(session) source_rows = [{"source_data": {"id": 1}}, {"source_data": {"id": 2}}] result = svc._filter_new_keys(job, "curr-run-3", source_rows) assert len(result) == 2 finally: session.close() Base.metadata.drop_all(bind=engine) def test_filter_new_keys_no_prev_records(self): """Prior run has no records -> all rows new.""" session, engine = make_session() try: job = TranslationJob(id="j4", name="j4", status="ACTIVE", source_dialect="en", target_dialect="fr", target_key_cols=["id"]) session.add(job) prev_run = TranslationRun(id="prev-run-4", job_id="j4", status="COMPLETED", insert_status="succeeded", started_at=datetime.now(UTC), created_at=datetime.now(UTC)) session.add(prev_run) session.commit() svc = self._make_service(session) source_rows = [{"source_data": {"id": 1}}] result = svc._filter_new_keys(job, "curr-run-4", source_rows) assert len(result) == 1 finally: session.close() Base.metadata.drop_all(bind=engine) def test_filter_new_keys_empty_key_cols_with_prev_records(self): """Prev records exist but key_cols is empty (None) → all rows pass through (lines 75-76).""" session, engine = make_session() try: job = TranslationJob(id="j-ekc", name="j-ekc", status="ACTIVE", source_dialect="en", target_dialect="fr", target_key_cols=None, source_key_cols=None) session.add(job) prev_run = TranslationRun(id="prev-run-ekc", job_id="j-ekc", status="COMPLETED", insert_status="succeeded", started_at=datetime.now(UTC), created_at=datetime.now(UTC)) session.add(prev_run) session.flush() # Add prev records so the code reaches the key_cols check batch = TranslationBatch(id="batch-ekc", run_id="prev-run-ekc", batch_index=0, status="COMPLETED", created_at=datetime.now(UTC)) session.add(batch) session.flush() rec = TranslationRecord(id="rec-ekc", run_id="prev-run-ekc", batch_id="batch-ekc", status="SUCCESS", source_data={"id": 1}) session.add(rec) session.commit() svc = self._make_service(session) source_rows = [{"source_data": {"id": 1}}, {"source_data": {"id": 2}}] result = svc._filter_new_keys(job, "curr-run-ekc", source_rows) # key_cols = None or None or [] = [] → not key_cols → True → return source_rows assert len(result) == 2 finally: session.close() Base.metadata.drop_all(bind=engine) # -- _load_preview_edits -- def test_load_preview_edits_no_session(self): """No applied preview session -> empty cache.""" session, engine = make_session() try: job = TranslationJob(id="j5", name="j5", status="ACTIVE", source_dialect="en", target_dialect="fr") session.add(job) session.commit() svc = self._make_service(session) svc._load_preview_edits("j5") assert svc._preview_edits_cache == {} finally: session.close() Base.metadata.drop_all(bind=engine) def test_load_preview_edits_skip_null_source_data(self): """Preview record with source_data=None is skipped (line 120).""" session, engine = make_session() try: job = TranslationJob(id="j-null-sd", name="j-null-sd", status="ACTIVE", source_dialect="en", target_dialect="fr") session.add(job) ps = TranslationPreviewSession(id="ps-null", job_id="j-null-sd", status="APPLIED", created_at=datetime.now(UTC)) session.add(ps) from src.models.translate import TranslationPreviewRecord as TPR rec = TPR(id="prec-null", session_id="ps-null", status="APPROVED", source_data=None) session.add(rec) session.commit() svc = self._make_service(session) svc._load_preview_edits("j-null-sd") assert svc._preview_edits_cache == {} finally: session.close() Base.metadata.drop_all(bind=engine) def test_load_preview_edits_with_edits(self): """Load preview edits from applied session.""" session, engine = make_session() try: job = TranslationJob(id="j6", name="j6", status="ACTIVE", source_dialect="en", target_dialect="fr") session.add(job) ps = TranslationPreviewSession(id="ps-1", job_id="j6", status="APPLIED", created_at=datetime.now(UTC)) session.add(ps) # The _load_preview_edits iterates rec.languages which is a Python attribute # on the model instance set after DB fetch, not a column. # We'll create a record with source_data, then patch the query to return # records with languages attached. from src.models.translate import TranslationPreviewRecord as TPR rec = TPR(id="prec-1", session_id="ps-1", status="APPROVED", source_data={"id": 1}) session.add(rec) session.commit() svc = self._make_service(session) # We need to mock the query to return a record with .languages class MockLangEntry: def __init__(self, code, status, user_edit=None, final_value=None): self.language_code = code self.status = status self.user_edit = user_edit self.final_value = final_value mock_rec = MagicMock(spec=TPR) mock_rec.source_data = {"id": 1} mock_rec.languages = [ MockLangEntry("fr", "edited", "bonjour", "bonjour"), MockLangEntry("de", "approved", "hallo"), ] with patch.object(session, 'query', return_value=MagicMock()) as mock_query: mock_filter = MagicMock() mock_filter.order_by.return_value.first.return_value = ps mock_query.return_value.filter.return_value = mock_filter # Mock the records query mock_filter2 = MagicMock() mock_filter2.all.return_value = [mock_rec] mock_query.return_value.filter.return_value = mock_filter # Make first call return ps, second call return records mock_query.return_value.filter.return_value.order_by.return_value.first.return_value = ps # For the records query mock_records_filter = MagicMock() mock_records_filter.all.return_value = [mock_rec] mock_query.return_value.filter.return_value = mock_records_filter svc._load_preview_edits("j6") assert svc._preview_edits_cache is not None finally: session.close() Base.metadata.drop_all(bind=engine) # -- _compute_key_hash -- def test_compute_key_hash(self): """_compute_key_hash returns deterministic 16-char hex.""" from src.plugins.translate._run_service import RunExecutionService h1 = RunExecutionService._compute_key_hash({"a": 1, "b": 2}) h2 = RunExecutionService._compute_key_hash({"a": 1, "b": 2}) h3 = RunExecutionService._compute_key_hash({"a": 1, "b": 3}) assert h1 == h2 assert h1 != h3 assert len(h1) == 16 # -- _update_language_stats_incremental -- def test_update_language_stats_empty(self): """No records -> no stats update (no-op).""" session, engine = make_session() try: svc = self._make_service(session) stats_map = {"fr": TranslationRunLanguageStats(run_id="r1", language_code="fr")} # No records for run-1, should be a no-op svc._update_language_stats_incremental("r1", stats_map) # Just check it doesn't raise finally: session.close() Base.metadata.drop_all(bind=engine) def test_update_language_stats_with_records(self): """Update stats from records.""" session, engine = make_session() try: run = TranslationRun(id="stats-run", job_id="no-job", status="COMPLETED", started_at=datetime.now(UTC)) session.add(run) session.flush() batch = TranslationBatch(id="stats-batch", run_id="stats-run", batch_index=0, status="COMPLETED", created_at=datetime.now(UTC)) session.add(batch) session.flush() rec = TranslationRecord(id="stats-rec", run_id="stats-run", batch_id="stats-batch", status="SUCCESS") session.add(rec) session.flush() lang = TranslationLanguage(id="l1", record_id="stats-rec", language_code="fr", status="translated", translated_value="bonjour") session.add(lang) session.commit() svc = self._make_service(session) lang_stat = TranslationRunLanguageStats(run_id="stats-run", language_code="fr", total_rows=0, translated_rows=0) stats_map = {"fr": lang_stat} svc._update_language_stats_incremental("stats-run", stats_map) assert lang_stat.total_rows == 1 assert lang_stat.translated_rows == 1 assert lang_stat.failed_rows == 0 assert lang_stat.skipped_rows == 0 finally: session.close() Base.metadata.drop_all(bind=engine) def test_update_language_stats_multiple_statuses(self): """Multiple language entries with different statuses.""" session, engine = make_session() try: run = TranslationRun(id="stats-run-2", job_id="no-job", status="COMPLETED", started_at=datetime.now(UTC)) session.add(run) session.flush() batch = TranslationBatch(id="stats-batch-2", run_id="stats-run-2", batch_index=0, status="COMPLETED", created_at=datetime.now(UTC)) session.add(batch) session.flush() # One record per unique (record_id, language_code) to avoid UNIQUE constraint rec_fr_translated = TranslationRecord(id="stats-rec-fr-t", run_id="stats-run-2", batch_id="stats-batch-2", status="SUCCESS") rec_fr_failed = TranslationRecord(id="stats-rec-fr-f", run_id="stats-run-2", batch_id="stats-batch-2", status="SUCCESS") rec_de_skipped = TranslationRecord(id="stats-rec-de-s", run_id="stats-run-2", batch_id="stats-batch-2", status="SUCCESS") rec_de_translated = TranslationRecord(id="stats-rec-de-t", run_id="stats-run-2", batch_id="stats-batch-2", status="SUCCESS") session.add_all([rec_fr_translated, rec_fr_failed, rec_de_skipped, rec_de_translated]) session.flush() entries_data = [ ("stats-rec-fr-t", "fr", "translated", "x"), ("stats-rec-fr-f", "fr", "failed", None), ("stats-rec-de-s", "de", "skipped", None), ("stats-rec-de-t", "de", "translated", "x"), ] for rec_id, code, st, tv in entries_data: session.add(TranslationLanguage( id=str(uuid.uuid4()), record_id=rec_id, language_code=code, status=st, translated_value=tv, )) session.commit() svc = self._make_service(session) fr_stat = TranslationRunLanguageStats(run_id="stats-run-2", language_code="fr") de_stat = TranslationRunLanguageStats(run_id="stats-run-2", language_code="de") stats_map = {"fr": fr_stat, "de": de_stat} svc._update_language_stats_incremental("stats-run-2", stats_map) assert fr_stat.total_rows == 2 assert fr_stat.translated_rows == 1 assert fr_stat.failed_rows == 1 assert de_stat.total_rows == 2 assert de_stat.translated_rows == 1 assert de_stat.skipped_rows == 1 finally: session.close() Base.metadata.drop_all(bind=engine)