- 9 new enhancement test files: test_connection_service.py, test_db_executor.py, test_orchestrator_direct_db.py, test_batch_insert.py, test_lang_stats.py, test_response_field_coverage.py, test_retry.py, test_run_service.py, test_sql_insert_service.py - 5 new integration tests: test_superset_sqllab_e2e.py, test_translate_clickhouse.py, test_translate_corrections.py, test_translate_schedules.py, test_translate_status_fk.py - Updated existing tests for insert_method/connection_id fields
246 lines
9.6 KiB
Python
246 lines
9.6 KiB
Python
# #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
|