228 lines
9.2 KiB
Python
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
|