- 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
468 lines
15 KiB
Python
468 lines
15 KiB
Python
# #region TranslateHistoryTests [C:2] [TYPE Module]
|
|
# @SEMANTICS: tests, translate, history, metrics
|
|
# @BRIEF Tests for run history list/detail endpoints and metrics aggregation.
|
|
# @LAYER Tests
|
|
# @RELATION BINDS_TO -> [TranslateRoutes]
|
|
# @RELATION BINDS_TO -> [TranslationMetrics]
|
|
#
|
|
# @TEST_CONTRACT: HistoryFlow ->
|
|
# {
|
|
# invariants: [
|
|
# "GET /api/translate/runs returns paginated list",
|
|
# "GET /api/translate/runs supports filters (job_id, status, trigger_type)",
|
|
# "GET /api/translate/runs/{id}/detail returns config_snapshot, records, events",
|
|
# "GET /api/translate/jobs/{id}/metrics returns aggregated data",
|
|
# "Metrics include run counts, record counts, duration"
|
|
# ]
|
|
# }
|
|
# @TEST_FIXTURE: completed_run -> run with COMPLETED status
|
|
# @TEST_EDGE: filter_by_job_id -> returns only that job's runs
|
|
# @TEST_EDGE: filter_by_status -> returns only matching status
|
|
# @TEST_EDGE: metrics_empty_job -> returns zero counts
|
|
# @TEST_INVARIANT: history_list_and_detail -> VERIFIED_BY: [completed_run, filter_by_status]
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from datetime import UTC, datetime
|
|
import pytest
|
|
from unittest.mock import MagicMock
|
|
import uuid
|
|
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from src.app import app
|
|
from src.core.database import Base
|
|
from src.dependencies import get_config_manager, get_current_user, get_db
|
|
from src.models.translate import TranslationEvent, TranslationJob, TranslationRun
|
|
|
|
# In-memory SQLite
|
|
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
|
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
|
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
mock_user = MagicMock()
|
|
mock_user.username = "testuser"
|
|
mock_user.roles = []
|
|
admin_role = MagicMock()
|
|
admin_role.name = "Admin"
|
|
mock_user.roles.append(admin_role)
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
connection = engine.connect()
|
|
transaction = connection.begin()
|
|
session = TestingSessionLocal(bind=connection)
|
|
yield session
|
|
session.close()
|
|
transaction.rollback()
|
|
connection.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_api_deps():
|
|
config_manager = MagicMock()
|
|
config_manager.get_environments.return_value = []
|
|
|
|
connection = engine.connect()
|
|
transaction = connection.begin()
|
|
session = TestingSessionLocal(bind=connection)
|
|
|
|
def override_get_db():
|
|
try:
|
|
yield session
|
|
finally:
|
|
pass
|
|
|
|
overrides = {
|
|
get_current_user: lambda: mock_user,
|
|
get_config_manager: lambda: config_manager,
|
|
get_db: override_get_db,
|
|
}
|
|
for dep, override in overrides.items():
|
|
app.dependency_overrides[dep] = override
|
|
|
|
yield {"config_manager": config_manager, "session": session}
|
|
|
|
app.dependency_overrides.clear()
|
|
session.close()
|
|
transaction.rollback()
|
|
connection.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(mock_api_deps):
|
|
return TestClient(app)
|
|
|
|
|
|
def _create_test_job(db_session) -> TranslationJob:
|
|
job = TranslationJob(
|
|
id=str(uuid.uuid4()),
|
|
name="Test Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
status="ACTIVE",
|
|
)
|
|
db_session.add(job)
|
|
db_session.flush()
|
|
return job
|
|
|
|
|
|
def _create_test_run(db_session, job_id: str, status: str = "COMPLETED", trigger: str = "manual") -> TranslationRun:
|
|
run = TranslationRun(
|
|
id=str(uuid.uuid4()),
|
|
job_id=job_id,
|
|
status=status,
|
|
trigger_type=trigger,
|
|
total_records=100,
|
|
successful_records=80,
|
|
failed_records=10,
|
|
skipped_records=10,
|
|
config_snapshot={"test": True},
|
|
config_hash="abc123",
|
|
dict_snapshot_hash="def456",
|
|
key_hash="ghi789",
|
|
created_by="testuser",
|
|
created_at=datetime.now(UTC),
|
|
)
|
|
db_session.add(run)
|
|
db_session.flush()
|
|
return run
|
|
|
|
|
|
# #region test_list_runs_empty [C:2] [TYPE Function]
|
|
# @BRIEF Verify runs list returns empty result initially.
|
|
def test_list_runs_empty(client):
|
|
"""Test runs list initially returns empty."""
|
|
response = client.get("/api/translate/runs")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 0
|
|
assert data["items"] == []
|
|
# #endregion test_list_runs_empty
|
|
|
|
|
|
# #region test_list_runs_with_data [C:2] [TYPE Function]
|
|
# @BRIEF Verify runs list returns data when runs exist.
|
|
def test_list_runs_with_data(client, mock_api_deps):
|
|
"""Test runs list with existing runs."""
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
_create_test_run(session, job.id)
|
|
session.commit()
|
|
|
|
response = client.get("/api/translate/runs")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] >= 1
|
|
assert len(data["items"]) >= 1
|
|
assert data["items"][0]["job_id"] == job.id
|
|
# #endregion test_list_runs_with_data
|
|
|
|
|
|
# #region test_list_runs_filter_job_id [C:2] [TYPE Function]
|
|
# @BRIEF Verify filtering runs by job_id works.
|
|
def test_list_runs_filter_job_id(client, mock_api_deps):
|
|
"""Test filtering runs by job_id."""
|
|
session = mock_api_deps["session"]
|
|
job1 = _create_test_job(session)
|
|
job2 = _create_test_job(session)
|
|
_create_test_run(session, job1.id)
|
|
_create_test_run(session, job2.id)
|
|
session.commit()
|
|
|
|
response = client.get(f"/api/translate/runs?job_id={job1.id}")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] >= 1
|
|
for item in data["items"]:
|
|
assert item["job_id"] == job1.id
|
|
# #endregion test_list_runs_filter_job_id
|
|
|
|
|
|
# #region test_list_runs_filter_status [C:2] [TYPE Function]
|
|
# @BRIEF Verify filtering runs by status works.
|
|
def test_list_runs_filter_status(client, mock_api_deps):
|
|
"""Test filtering runs by status."""
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
_create_test_run(session, job.id, status="COMPLETED")
|
|
_create_test_run(session, job.id, status="FAILED")
|
|
session.commit()
|
|
|
|
response = client.get("/api/translate/runs?run_status=FAILED")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
for item in data["items"]:
|
|
assert item["status"] == "FAILED"
|
|
# #endregion test_list_runs_filter_status
|
|
|
|
|
|
# #region test_get_run_detail [C:2] [TYPE Function]
|
|
# @BRIEF Verify run detail returns config_snapshot, records, events.
|
|
def test_get_run_detail(client, mock_api_deps):
|
|
"""Test run detail endpoint."""
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
run = _create_test_run(session, job.id)
|
|
|
|
# Add an event
|
|
event = TranslationEvent(
|
|
id=str(uuid.uuid4()),
|
|
job_id=job.id,
|
|
run_id=run.id,
|
|
event_type="RUN_STARTED",
|
|
event_data={},
|
|
created_by="testuser",
|
|
created_at=datetime.now(UTC),
|
|
)
|
|
session.add(event)
|
|
session.commit()
|
|
|
|
response = client.get(f"/api/translate/runs/{run.id}/detail")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == run.id
|
|
assert data["config_snapshot"] == {"test": True}
|
|
assert data["config_hash"] == "abc123"
|
|
assert len(data["events"]) >= 1
|
|
assert data["events"][0]["event_type"] == "RUN_STARTED"
|
|
# #endregion test_get_run_detail
|
|
|
|
|
|
# #region test_get_job_metrics [C:2] [TYPE Function]
|
|
# @BRIEF Verify metrics endpoint returns aggregated data.
|
|
def test_get_job_metrics(client, mock_api_deps):
|
|
"""Test job metrics endpoint."""
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
_create_test_run(session, job.id, status="COMPLETED")
|
|
_create_test_run(session, job.id, status="FAILED")
|
|
session.commit()
|
|
|
|
response = client.get(f"/api/translate/jobs/{job.id}/metrics")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["job_id"] == job.id
|
|
assert data["total_runs"] >= 2
|
|
assert data["successful_runs"] >= 1
|
|
assert data["failed_runs"] >= 1
|
|
# #endregion test_get_job_metrics
|
|
|
|
|
|
# #region test_get_all_metrics [C:2] [TYPE Function]
|
|
# @BRIEF Verify global metrics endpoint returns data.
|
|
def test_get_all_metrics(client, mock_api_deps):
|
|
"""Test global metrics endpoint."""
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
_create_test_run(session, job.id)
|
|
session.commit()
|
|
|
|
response = client.get("/api/translate/metrics")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 1
|
|
# #endregion test_get_all_metrics
|
|
|
|
|
|
# #region test_metrics_empty_job [C:2] [TYPE Function]
|
|
# @BRIEF Verify metrics for job with no runs returns zeros.
|
|
def test_metrics_empty_job(client, mock_api_deps):
|
|
"""Test metrics for job with no runs."""
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
session.commit()
|
|
|
|
response = client.get(f"/api/translate/jobs/{job.id}/metrics")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total_runs"] == 0
|
|
# #endregion test_metrics_empty_job
|
|
|
|
|
|
# #region test_run_detail_not_found [C:2] [TYPE Function]
|
|
# @BRIEF Verify 404 on non-existent run detail.
|
|
def test_run_detail_not_found(client):
|
|
"""Test run detail returns 404 for non-existent run."""
|
|
response = client.get("/api/translate/runs/non-existent/detail")
|
|
assert response.status_code == 404
|
|
# #endregion test_run_detail_not_found
|
|
# #region test_list_runs_includes_language_info [C:2] [TYPE Function]
|
|
# @BRIEF Verify run list includes target_languages, total_translated, and language_stats.
|
|
def test_list_runs_includes_language_info(client, mock_api_deps):
|
|
"""Test run list includes per-language fields."""
|
|
from src.models.translate import TranslationRunLanguageStats
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
run = _create_test_run(session, job.id)
|
|
# Add language stats
|
|
ls = TranslationRunLanguageStats(
|
|
id=str(uuid.uuid4()),
|
|
run_id=run.id,
|
|
language_code="ru",
|
|
total_rows=100,
|
|
translated_rows=80,
|
|
failed_rows=5,
|
|
skipped_rows=15,
|
|
token_count=15000,
|
|
estimated_cost=0.03,
|
|
)
|
|
session.add(ls)
|
|
session.commit()
|
|
|
|
response = client.get("/api/translate/runs")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["items"]) >= 1
|
|
item = data["items"][0]
|
|
assert "target_languages" in item
|
|
assert "total_translated" in item
|
|
assert "language_stats" in item
|
|
assert "ru" in item["target_languages"]
|
|
assert item["total_translated"] == 80
|
|
assert item["language_stats"]["ru"]["translated_rows"] == 80
|
|
# #endregion test_list_runs_includes_language_info
|
|
|
|
|
|
# #region test_metrics_per_language_breakdown [C:2] [TYPE Function]
|
|
# @BRIEF Verify metrics endpoint returns per_language_metrics.
|
|
def test_metrics_per_language_breakdown(client, mock_api_deps):
|
|
"""Test metrics returns per-language breakdown."""
|
|
from src.models.translate import TranslationRunLanguageStats
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
run = _create_test_run(session, job.id)
|
|
ls = TranslationRunLanguageStats(
|
|
id=str(uuid.uuid4()),
|
|
run_id=run.id,
|
|
language_code="ru",
|
|
total_rows=100,
|
|
translated_rows=80,
|
|
token_count=15000,
|
|
estimated_cost=0.03,
|
|
)
|
|
session.add(ls)
|
|
session.commit()
|
|
|
|
response = client.get(f"/api/translate/jobs/{job.id}/metrics")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "per_language_metrics" in data
|
|
assert "ru" in data["per_language_metrics"]
|
|
assert data["per_language_metrics"]["ru"]["tokens"] == 15000
|
|
assert data["per_language_metrics"]["ru"]["cost"] == 0.03
|
|
# #endregion test_metrics_per_language_breakdown
|
|
|
|
|
|
# #region test_metric_snapshot_stores_per_language [C:2] [TYPE Function]
|
|
# @BRIEF Verify MetricSnapshot stores per_language_metrics via prune_expired.
|
|
def test_metric_snapshot_stores_per_language(client, mock_api_deps):
|
|
"""Test MetricSnapshot stores per_language_metrics after prune."""
|
|
from datetime import timedelta
|
|
|
|
from src.models.translate import MetricSnapshot, TranslationEvent
|
|
from src.plugins.translate.events import TranslationEventLog
|
|
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
session.commit()
|
|
|
|
# Create an old event with per-language data
|
|
old_date = datetime.now(UTC) - timedelta(days=100)
|
|
event = TranslationEvent(
|
|
id=str(uuid.uuid4()),
|
|
job_id=job.id,
|
|
event_type="TRANSLATION_PHASE_COMPLETED",
|
|
event_data={
|
|
"language_code": "ru",
|
|
"token_count": 5000,
|
|
"cost": 0.01,
|
|
"run_id": "test-run-id",
|
|
},
|
|
created_at=old_date,
|
|
)
|
|
session.add(event)
|
|
session.commit()
|
|
|
|
# Prune events older than 30 days
|
|
event_log = TranslationEventLog(session)
|
|
result = event_log.prune_expired(retention_days=30)
|
|
assert result["snapshot_id"] is not None
|
|
|
|
# Verify snapshot has per_language_metrics
|
|
snapshot = session.query(MetricSnapshot).filter(
|
|
MetricSnapshot.id == result["snapshot_id"]
|
|
).first()
|
|
assert snapshot is not None
|
|
assert snapshot.per_language_metrics is not None
|
|
assert "ru" in snapshot.per_language_metrics
|
|
assert snapshot.per_language_metrics["ru"]["cumulative_tokens"] == 5000
|
|
assert snapshot.per_language_metrics["ru"]["cumulative_cost"] == 0.01
|
|
# #endregion test_metric_snapshot_stores_per_language
|
|
|
|
|
|
# #region test_combined_metrics_live_and_snapshot [C:2] [TYPE Function]
|
|
# @BRIEF Verify combined metrics merge live + snapshot per-language data.
|
|
def test_combined_metrics_live_and_snapshot(client, mock_api_deps):
|
|
"""Test metrics merge live data with MetricSnapshot per-language data."""
|
|
from datetime import timedelta
|
|
|
|
from src.models.translate import (
|
|
MetricSnapshot,
|
|
TranslationRunLanguageStats,
|
|
)
|
|
from src.plugins.translate.metrics import TranslationMetrics
|
|
|
|
session = mock_api_deps["session"]
|
|
job = _create_test_job(session)
|
|
run = _create_test_run(session, job.id)
|
|
session.commit()
|
|
|
|
# Add snapshot with per-language data (simulating pruned period)
|
|
snapshot = MetricSnapshot(
|
|
id=str(uuid.uuid4()),
|
|
job_id=job.id,
|
|
key_hash="test_key",
|
|
per_language_metrics={
|
|
"ru": {"cumulative_tokens": 100000, "cumulative_cost": 0.20, "runs": 3},
|
|
},
|
|
covers_events_before=datetime.now(UTC) - timedelta(days=90),
|
|
total_records=100,
|
|
snapshot_date=datetime.now(UTC),
|
|
)
|
|
session.add(snapshot)
|
|
|
|
# Add live language stats (recent runs)
|
|
ls = TranslationRunLanguageStats(
|
|
id=str(uuid.uuid4()),
|
|
run_id=run.id,
|
|
language_code="ru",
|
|
total_rows=50,
|
|
translated_rows=40,
|
|
token_count=5000,
|
|
estimated_cost=0.01,
|
|
)
|
|
session.add(ls)
|
|
session.commit()
|
|
|
|
metrics_svc = TranslationMetrics(session)
|
|
result = metrics_svc.get_job_metrics(job.id)
|
|
assert "per_language_metrics" in result
|
|
assert "ru" in result["per_language_metrics"]
|
|
# Live 5000 + snapshot 100000 = 105000
|
|
assert result["per_language_metrics"]["ru"]["tokens"] == 105000
|
|
# Live 0.01 + snapshot 0.20 = 0.21
|
|
assert abs(result["per_language_metrics"]["ru"]["cost"] - 0.21) < 0.001
|
|
# Live 1 + snapshot 3 = 4
|
|
assert result["per_language_metrics"]["ru"]["runs"] == 4
|
|
# #endregion test_combined_metrics_live_and_snapshot
|
|
# #endregion TranslateHistoryTests
|