Files
ss-tools/backend/tests/plugins/translate/test_orchestrator_cancel.py

228 lines
9.2 KiB
Python

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