# #region Test.OrchestratorCancel [C:3] [TYPE Module] [SEMANTICS test, translate, cancel, retry] # @BRIEF Verify orchestrator_cancel contracts — cancel_run and retry_insert. # @RELATION BINDS_TO -> [orchestrator_cancel] # @TEST_EDGE: run_not_found -> cancel_run raises ValueError # @TEST_EDGE: invalid_status -> cancel_run raises ValueError for COMPLETED/FAILED runs # @TEST_EDGE: lock_timeout -> cancel_run falls back to _fallback_cancel_request # @TEST_EDGE: retry_insert_run_not_found -> retry_insert raises ValueError # @TEST_EDGE: retry_insert_job_not_found -> retry_insert raises ValueError import pytest from unittest.mock import AsyncMock, MagicMock, patch from datetime import UTC, datetime import uuid from sqlalchemy.orm import Session from src.models.translate import ( TranslationJob, TranslationRun, TranslationEvent, ) from src.plugins.translate.events import TranslationEventLog from .conftest import JOB_ID, RUN_ID class _MockExecute: """Helper: patch session.execute so SET LOCAL succeeds for SQLite compat.""" @staticmethod def _make_passthrough(session): """Return a side_effect that passes thru SET LOCAL, delegates real SQL.""" real_execute = session.execute def passthrough(statement, *args, **kwargs): sql_str = str(statement) if 'SET LOCAL' in sql_str or 'lock_timeout' in sql_str: return # silently pass PostgreSQL-specific SET LOCAL return real_execute(statement, *args, **kwargs) return passthrough class TestCancelRun: """Verify cancel_run function.""" def _patch_set_local(self, session): """Patch session.execute so SET LOCAL succeeds.""" return patch.object(session, 'execute', side_effect=_MockExecute._make_passthrough(session)) def test_cancel_run_success(self, db_with_run): """Happy path: cancel a RUNNING run.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "RUNNING" session.commit() event_log = TranslationEventLog(session) from src.plugins.translate.orchestrator_cancel import cancel_run with self._patch_set_local(session): result = cancel_run(session, event_log, "test_user", run_id) assert result.status == "CANCELLED" assert result.completed_at is not None events = session.query(TranslationEvent).filter( TranslationEvent.run_id == run_id ).all() assert any(e.event_type == "RUN_CANCELLED" for e in events) def test_cancel_run_not_found(self, db_session): """Negative: run_id does not exist.""" event_log = TranslationEventLog(db_session) from src.plugins.translate.orchestrator_cancel import cancel_run with pytest.raises(ValueError, match="not found"): with self._patch_set_local(db_session): cancel_run(db_session, event_log, "test_user", "nonexistent-id") def test_cancel_run_invalid_status(self, db_with_run): """Negative: cannot cancel COMPLETED run.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "COMPLETED" session.commit() event_log = TranslationEventLog(session) from src.plugins.translate.orchestrator_cancel import cancel_run with pytest.raises(ValueError, match="Cannot cancel"): with self._patch_set_local(session): cancel_run(session, event_log, "test_user", run_id) def test_cancel_run_fail_status(self, db_with_run): """Negative: cannot cancel FAILED run.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "FAILED" session.commit() event_log = TranslationEventLog(session) from src.plugins.translate.orchestrator_cancel import cancel_run with pytest.raises(ValueError, match="Cannot cancel"): with self._patch_set_local(session): cancel_run(session, event_log, "test_user", run_id) def test_cancel_run_lock_timeout_fallback(self, db_with_run): """Edge: lock timeout on execute -> _fallback_cancel_request.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "RUNNING" session.commit() event_log = TranslationEventLog(session) from src.plugins.translate.orchestrator_cancel import cancel_run _exec_count = [0] real_exec = session.execute def _fail_first_then_real(statement, *args, **kwargs): _exec_count[0] += 1 if _exec_count[0] == 1: raise Exception("lock timeout") return real_exec(statement, *args, **kwargs) with patch.object(session, 'execute', side_effect=_fail_first_then_real): result = cancel_run(session, event_log, "test_user", run_id) assert result is not None def test_cancel_run_flush_fallback(self, db_with_run): """Edge: flush failure triggers _fallback_cancel_request.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "RUNNING" session.commit() event_log = TranslationEventLog(session) from src.plugins.translate.orchestrator_cancel import cancel_run _flush_count = [0] def _fail_once(): _flush_count[0] += 1 if _flush_count[0] == 1: raise Exception("flush failed") with self._patch_set_local(session), patch.object(session, 'flush', side_effect=_fail_once): result = cancel_run(session, event_log, "test_user", run_id) assert result is not None def test_cancel_run_fallback_no_run_found(self): """_fallback_cancel_request with nonexistent run raises ValueError.""" session, engine = None, None from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from src.plugins.translate.orchestrator_cancel import _fallback_cancel_request from src.models.translate import Base engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(bind=engine) session = sessionmaker(bind=engine)() try: with pytest.raises(ValueError, match="not found"): _fallback_cancel_request(session, "nonexistent-run-id") finally: session.close() class TestRetryInsert: """Verify retry_insert function.""" @pytest.mark.asyncio async def test_retry_insert_run_not_found(self, db_session): """Negative: run_id does not exist.""" event_log = TranslationEventLog(db_session) config_manager = MagicMock() from src.plugins.translate.orchestrator_cancel import retry_insert with pytest.raises(ValueError, match="not found"): await retry_insert(db_session, config_manager, event_log, "test_user", "nonexistent-id") @pytest.mark.asyncio async def test_retry_insert_job_not_found(self, db_with_run): """Negative: run's job does not exist (deleted).""" session, run_id = db_with_run session.query(TranslationJob).filter(TranslationJob.id == JOB_ID).delete() session.commit() event_log = TranslationEventLog(session) config_manager = MagicMock() from src.plugins.translate.orchestrator_cancel import retry_insert with pytest.raises(ValueError, match="not found"): await retry_insert(session, config_manager, event_log, "test_user", run_id) @pytest.mark.asyncio async def test_retry_insert_success(self, db_with_run): """Happy path: retry_insert completes successfully.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "COMPLETED" session.commit() event_log = TranslationEventLog(session) config_manager = MagicMock() from src.plugins.translate.orchestrator_cancel import retry_insert with patch('src.plugins.translate.orchestrator_cancel.SQLInsertService') as MockSvc: mock_svc = MockSvc.return_value mock_svc.generate_and_insert_sql = AsyncMock(return_value={ "status": "success", "query_id": "q123" }) result = await retry_insert(session, config_manager, event_log, "test_user", run_id) assert result.insert_status == "success" assert result.superset_execution_id == "q123" events = session.query(TranslationEvent).filter( TranslationEvent.run_id == run_id ).all() assert any(e.event_type == "RUN_RETRY_INSERT" for e in events) @pytest.mark.asyncio async def test_retry_insert_with_error(self, db_with_run): """Edge: insert_result contains error_message.""" session, run_id = db_with_run run = session.query(TranslationRun).filter(TranslationRun.id == run_id).first() run.status = "COMPLETED" session.commit() event_log = TranslationEventLog(session) config_manager = MagicMock() from src.plugins.translate.orchestrator_cancel import retry_insert with patch('src.plugins.translate.orchestrator_cancel.SQLInsertService') as MockSvc: mock_svc = MockSvc.return_value mock_svc.generate_and_insert_sql = AsyncMock(return_value={ "status": "error", "query_id": None, "error_message": "DB connection failed" }) result = await retry_insert(session, config_manager, event_log, "test_user", run_id) assert result.error_message == "DB connection failed" assert result.superset_execution_id == "" # #endregion Test.OrchestratorCancel