# #region Test.Api.TranslateRunRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,runs,execution,api] # @BRIEF Tests for _run_routes.py — run execution, retry, cancel. # @RELATION BINDS_TO -> [TranslateRunRoutesModule] # @TEST_EDGE: run_value_error -> 400 # @TEST_EDGE: run_generic_error -> 502 # @TEST_EDGE: retry_value_error -> 400 # @TEST_EDGE: retry_generic_error -> 502 import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from unittest.mock import AsyncMock _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) def _make_client(overrides: dict | None = None) -> TestClient: from src.api.routes.translate._run_routes import router from src.dependencies import ( get_config_manager, get_current_user, get_db, has_permission, require_feature, ) from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router) mock_user = User( id="user-1", username="admin", email="admin@x.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])], ) defaults = { get_current_user: lambda: mock_user, get_config_manager: lambda: MagicMock(), get_db: lambda: MagicMock(), has_permission: lambda *a, **kw: lambda: None, require_feature: lambda *a, **kw: lambda: None, } if overrides: defaults.update(overrides) for dep, mock_fn in defaults.items(): app.dependency_overrides[dep] = mock_fn return TestClient(app) def _mock_run(id="run-1", job_id="job-1", status="PENDING"): r = MagicMock() r.id = id r.job_id = job_id r.status = status r.trigger_type = "manual" r.started_at = None r.completed_at = None r.error_message = None r.total_records = 0 r.successful_records = 0 r.failed_records = 0 r.skipped_records = 0 r.cache_hits = 0 r.insert_status = None r.superset_execution_id = None r.config_snapshot = None r.key_hash = None r.config_hash = None r.dict_snapshot_hash = None r.created_by = "admin" r.created_at = __import__("datetime").datetime(2026, 1, 1) return r # ============================================================================= # Run Translation # ============================================================================= class TestRunTranslation: """POST /api/translate/jobs/{job_id}/run""" def test_run_translation_success(self): mock_orch = MagicMock() mock_orch.start_run.return_value = _mock_run() with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.asyncio.create_task"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 201 data = resp.json() assert data["id"] == "run-1" assert data["status"] == "PENDING" def test_run_translation_full(self): mock_orch = MagicMock() mock_orch.start_run.return_value = _mock_run() with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.asyncio.create_task"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run?full_translation=true") assert resp.status_code == 201 # Verify start_run was called with full_translation=True mock_orch.start_run.assert_called_once_with( job_id="job-1", is_scheduled=False, full_translation=True ) def test_run_translation_value_error_400(self): mock_orch = MagicMock() mock_orch.start_run.side_effect = ValueError("Job not found") with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.asyncio.create_task"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-missing/run") assert resp.status_code == 400 assert "Job not found" in resp.text def test_run_translation_generic_error_502(self): mock_orch = MagicMock() mock_orch.start_run.side_effect = RuntimeError("Unexpected error") with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.asyncio.create_task"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 502 assert "Run failed" in resp.text # ============================================================================= # Retry Run # ============================================================================= class TestRetryRun: """POST /api/translate/runs/{run_id}/retry""" def test_retry_success(self): mock_orch = MagicMock() mock_orch.retry_failed_batches = AsyncMock(return_value=_mock_run(status="RUNNING")) with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/retry") assert resp.status_code == 200 assert resp.json()["status"] == "RUNNING" def test_retry_value_error_400(self): mock_orch = MagicMock() mock_orch.retry_failed_batches.side_effect = ValueError("Run not retriable") with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/retry") assert resp.status_code == 400 def test_retry_generic_error_502(self): mock_orch = MagicMock() mock_orch.retry_failed_batches.side_effect = RuntimeError("Internal error") with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/retry") assert resp.status_code == 502 assert "Retry failed" in resp.text # ============================================================================= # Retry Insert # ============================================================================= class TestRetryInsert: """POST /api/translate/runs/{run_id}/retry-insert""" def test_retry_insert_success(self): mock_orch = MagicMock() mock_orch.retry_insert = AsyncMock(return_value=_mock_run(status="COMPLETED")) with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/retry-insert") assert resp.status_code == 200 assert resp.json()["status"] == "COMPLETED" def test_retry_insert_value_error_400(self): mock_orch = MagicMock() mock_orch.retry_insert.side_effect = ValueError("Insert not needed") with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/retry-insert") assert resp.status_code == 400 def test_retry_insert_generic_error_502(self): mock_orch = MagicMock() mock_orch.retry_insert.side_effect = RuntimeError("Insert failed") with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/retry-insert") assert resp.status_code == 502 assert "Retry insert failed" in resp.text # ============================================================================= # Background Execute — covers _background_execute inner function (lines 57-120) # ============================================================================= class TestBackgroundExecute: """Execute the _background_execute coroutine via real asyncio.create_task.""" def test_bg_run_not_found(self): """Run not found in background: early return (lines 71-77).""" mock_orch = MagicMock() mock_orch.start_run.return_value = _mock_run() mock_bg_db = MagicMock() mock_bg_db.query.return_value.filter.return_value.first.return_value = None with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.SessionLocal", return_value=mock_bg_db), patch("src.api.routes.translate._run_routes.seed_trace_id"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 201 mock_bg_db.close.assert_called_once() def test_bg_execute_success(self): """Background execute succeeds (lines 65-85).""" mock_run = _mock_run(status="COMPLETED") mock_run.completed_at = __import__("datetime").datetime(2026, 1, 1) mock_orch = MagicMock() mock_orch.start_run.return_value = _mock_run() mock_orch.execute_run = AsyncMock() mock_bg_db = MagicMock() # Two queries: first to find run, second to verify mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run, mock_run] with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.SessionLocal", return_value=mock_bg_db), patch("src.api.routes.translate._run_routes.seed_trace_id"), patch("src.api.routes.translate._run_routes.get_trace_id", return_value="trace-abc-123"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 201 mock_orch.execute_run.assert_awaited_once() mock_bg_db.close.assert_called_once() def test_bg_execute_not_terminal_marks_failed(self): """Run not in terminal state after commit: marked FAILED (lines 86-95).""" mock_run_pending = _mock_run(status="RUNNING") mock_orch = MagicMock() mock_orch.start_run.return_value = _mock_run() mock_orch.execute_run = AsyncMock() mock_bg_db = MagicMock() mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run_pending, mock_run_pending] with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.SessionLocal", return_value=mock_bg_db), patch("src.api.routes.translate._run_routes.seed_trace_id"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 201 # Run should have been marked FAILED assert mock_run_pending.status == "FAILED" mock_bg_db.commit.assert_called() mock_bg_db.close.assert_called_once() def test_bg_execute_exception_marks_failed(self): """Exception in background marks run FAILED via fallback (lines 97-120).""" mock_run = _mock_run() mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch.execute_run = AsyncMock(side_effect=RuntimeError("LLM timeout")) mock_bg_db = MagicMock() mock_bg_db.query.return_value.filter.return_value.first.side_effect = [mock_run] # Second session for fallback mock_fb_db = MagicMock() mock_fb_db.query.return_value.filter.return_value.first.return_value = mock_run with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.SessionLocal", side_effect=[mock_bg_db, mock_fb_db]), patch("src.api.routes.translate._run_routes.seed_trace_id"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 201 assert mock_run.status == "FAILED" assert mock_run.error_message is not None mock_fb_db.close.assert_called_once() mock_bg_db.close.assert_called_once() def test_bg_execute_exception_fallback_fails(self): """Exception in background AND fallback: handled gracefully (lines 115-117).""" mock_run = _mock_run() mock_orch = MagicMock() mock_orch.start_run.return_value = mock_run mock_orch.execute_run = AsyncMock(side_effect=RuntimeError("LLM timeout")) mock_bg_db = MagicMock() mock_bg_db.query.return_value.filter.return_value.first.return_value = mock_run # Fallback db also raises bad_fb_db = MagicMock() bad_fb_db.query.side_effect = RuntimeError("DB closed") with ( patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch), patch("src.api.routes.translate._run_routes.SessionLocal", side_effect=[mock_bg_db, bad_fb_db]), patch("src.api.routes.translate._run_routes.seed_trace_id"), ): client = _make_client() resp = client.post("/api/translate/jobs/job-1/run") assert resp.status_code == 201 mock_bg_db.close.assert_called_once() # ============================================================================= # Cancel Run # ============================================================================= class TestCancelRun: """POST /api/translate/runs/{run_id}/cancel""" def test_cancel_success(self): mock_orch = MagicMock() mock_orch.cancel_run.return_value = _mock_run(status="CANCELLED") with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-1/cancel") assert resp.status_code == 200 assert resp.json()["status"] == "CANCELLED" def test_cancel_value_error_400(self): mock_orch = MagicMock() mock_orch.cancel_run.side_effect = ValueError("Run not found") with patch("src.api.routes.translate._run_routes.TranslationOrchestrator", return_value=mock_orch): client = _make_client() resp = client.post("/api/translate/runs/run-missing/cancel") assert resp.status_code == 400 # #endregion Test.Api.TranslateRunRoutes