# #region TestTranslateStatusAndFK [C:3] [TYPE Module] [SEMANTICS test,translate,integration,status,fk,constraints] # @BRIEF Integration tests for job status transitions, run state machine, FK constraint enforcement # with real PostgreSQL via Testcontainers. # @RELATION BINDS_TO -> [TranslateJobService] # @RELATION BINDS_TO -> [TranslationEventLog] # # @TEST_CONTRACT JobStatusTransitions -> # DRAFT job: create -> delete works # non-existent: get/update/delete raises ValueError # long name: persists # # @TEST_CONTRACT RunStateMachine -> # PENDING -> COMPLETED: via cancel_run # COMPLETED -> cancel raises ValueError # CANCELLED -> cancel raises ValueError # # @TEST_EDGE: delete_non_existent_job -> ValueError # @TEST_EDGE: cancel_non_existent_run -> ValueError # @TEST_EDGE: cancel_cancelled_run -> ValueError import pytest import uuid from sqlalchemy import text from sqlalchemy.orm import Session from src.models.translate import ( TranslationJob, TranslationRun, TranslationBatch, TranslationRecord, TranslationLanguage, TranslationEvent, ) from src.plugins.translate.service import TranslateJobService from src.schemas.translate import TranslateJobCreate # #region TestJobStatusTransitions [C:3] [TYPE Class] @pytest.mark.asyncio class TestJobStatusTransitions: """Verify job status lifecycle transitions with real PostgreSQL.""" async def test_create_draft_job(self, db_session: Session, mock_config_manager): service = TranslateJobService(db_session, mock_config_manager, "test_user") payload = TranslateJobCreate( name="Draft Test Job", source_dialect="postgresql", target_dialect="clickhouse", translation_column="name", target_languages=["ru"], ) job = await service.create_job(payload) assert job.status == "DRAFT" async def test_delete_job(self, db_session: Session, mock_config_manager): service = TranslateJobService(db_session, mock_config_manager, "test_user") payload = TranslateJobCreate( name="Delete Test", source_dialect="postgresql", target_dialect="clickhouse", translation_column="name", target_languages=["ru"], ) job = await service.create_job(payload) service.delete_job(job.id) with pytest.raises(ValueError, match="not found"): service.get_job(job.id) async def test_delete_non_existent_job(self, db_session: Session, mock_config_manager): service = TranslateJobService(db_session, mock_config_manager, "test_user") with pytest.raises(ValueError, match="not found"): service.delete_job("non-existent-id") async def test_get_non_existent_job(self, db_session: Session, mock_config_manager): service = TranslateJobService(db_session, mock_config_manager, "test_user") with pytest.raises(ValueError, match="not found"): service.get_job("non-existent-id") async def test_create_job_max_name_length(self, db_session: Session, mock_config_manager): service = TranslateJobService(db_session, mock_config_manager, "test_user") long_name = "A" * 500 payload = TranslateJobCreate( name=long_name, source_dialect="postgresql", target_dialect="clickhouse", translation_column="name", target_languages=["ru"], ) job = await service.create_job(payload) assert len(job.name) == 500 # #endregion TestJobStatusTransitions # #region TestFKConstraintEnforcement [C:3] [TYPE Class] class TestFKConstraintEnforcement: """Verify PostgreSQL FK constraints catch orphaned data.""" def _assert_fk_violation(self, db: Session, obj): """Helper: try adding an orphaned object and expect FK violation.""" db.add(obj) try: db.flush() db.rollback() pytest.fail("Expected FK violation was not raised") except Exception: db.rollback() def test_orphan_run_rejected(self, db_session: Session): run = TranslationRun(job_id=str(uuid.uuid4()), status="PENDING", trigger_type="manual") self._assert_fk_violation(db_session, run) def test_orphan_batch_rejected(self, db_session: Session): batch = TranslationBatch(run_id=str(uuid.uuid4()), batch_index=0, status="PENDING") self._assert_fk_violation(db_session, batch) def test_valid_run_with_job_succeeds(self, db_session: Session): job = TranslationJob( name="FK Test Job", status="ACTIVE", source_dialect="postgresql", target_dialect="clickhouse", created_by="test_user", ) db_session.add(job) db_session.flush() db_session.refresh(job) run = TranslationRun(job_id=job.id, status="PENDING", trigger_type="manual") db_session.add(run) db_session.flush() assert run.job_id == job.id # #endregion TestFKConstraintEnforcement # #region TestRunRecovery [C:3] [TYPE Class] class TestRunRecovery: """Verify run cancel behavior with real PostgreSQL.""" def _create_run(self, db: Session, status: str = "PENDING") -> TranslationRun: job = TranslationJob( name="Cancel Test", status="ACTIVE", source_dialect="postgresql", target_dialect="clickhouse", created_by="test_user", ) db.add(job) db.flush() db.refresh(job) run = TranslationRun(job_id=job.id, status=status, trigger_type="manual") db.add(run) db.flush() db.refresh(run) return run def test_cancel_pending_run(self, db_session: Session): run = self._create_run(db_session, "PENDING") from src.plugins.translate.orchestrator_cancel import cancel_run from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) result = cancel_run(db_session, event_log, "test_user", run.id) assert result.status == "CANCELLED" assert result.completed_at is not None def test_cancel_failed_run(self, db_session: Session): run = self._create_run(db_session, "FAILED") from src.plugins.translate.orchestrator_cancel import cancel_run from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) with pytest.raises(ValueError, match="Cannot cancel"): cancel_run(db_session, event_log, "test_user", run.id) def test_cancel_completed_run_raises(self, db_session: Session): run = self._create_run(db_session, "COMPLETED") from src.plugins.translate.orchestrator_cancel import cancel_run from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) with pytest.raises(ValueError, match="Cannot cancel"): cancel_run(db_session, event_log, "test_user", run.id) def test_cancel_cancelled_run_raises(self, db_session: Session): run = self._create_run(db_session, "CANCELLED") from src.plugins.translate.orchestrator_cancel import cancel_run from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) with pytest.raises(ValueError, match="Cannot cancel"): cancel_run(db_session, event_log, "test_user", run.id) def test_cancel_non_existent_run(self, db_session: Session): from src.plugins.translate.orchestrator_cancel import cancel_run from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) with pytest.raises((ValueError, KeyError)): cancel_run(db_session, event_log, "test_user", str(uuid.uuid4())) # #endregion TestRunRecovery # #region TestEventLogStateMachine [C:3] [TYPE Class] class TestEventLogStateMachine: """Verify event log state machine rules with real PostgreSQL.""" def _create_job_and_run(self, db: Session) -> tuple: job = TranslationJob( name="Event Log Job", status="ACTIVE", source_dialect="postgresql", target_dialect="clickhouse", created_by="test_user", ) db.add(job) db.flush() db.refresh(job) run = TranslationRun(job_id=job.id, status="RUNNING", trigger_type="manual") db.add(run) db.flush() db.refresh(run) return job, run def test_event_log_persistence(self, db_session: Session): job, run = self._create_job_and_run(db_session) from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) event_log.log_event(job.id, "RUN_STARTED", {"strategy": "incremental"}, run.id) event_log.log_event(job.id, "BATCH_STARTED", {"batch_index": 0}, run.id) event_log.log_event(job.id, "BATCH_COMPLETED", {"batch_index": 0, "records": 10}, run.id) event_log.log_event(job.id, "RUN_COMPLETED", {"total_records": 10}, run.id) events = ( db_session.query(TranslationEvent) .filter(TranslationEvent.run_id == run.id) .order_by(TranslationEvent.created_at) .all() ) assert len(events) == 4 def test_invalid_event_type_raises(self, db_session: Session): job, run = self._create_job_and_run(db_session) from src.plugins.translate.events import TranslationEventLog event_log = TranslationEventLog(db_session) with pytest.raises(ValueError, match="Invalid event_type"): event_log.log_event(job.id, run.id, "INVALID_EVENT", {}) # #endregion TestEventLogStateMachine # #endregion TestTranslateStatusAndFK