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

298 lines
13 KiB
Python

# #region Test.TranslationEventLog [C:3] [TYPE Module] [SEMANTICS test, event, log, audit]
# @BRIEF Verify TranslationEventLog contracts — log_event, query_events, prune_expired, get_run_event_summary.
# @RELATION BINDS_TO -> [TranslationEventLog]
# @TEST_EDGE: invalid_event_type -> Raises ValueError
# @TEST_EDGE: terminal_event_twice -> Raises ValueError
# @TEST_EDGE: missing_run_started -> Raises ValueError for run events
# @TEST_EDGE: prune_expired_noop -> Returns zero when no expired events
# @TEST_EDGE: prune_expired_creates_snapshot -> MetricSnapshot created before pruning
import pytest
from unittest.mock import MagicMock, patch
from datetime import UTC, datetime, timedelta
from sqlalchemy.orm import Session
from src.models.translate import (
TranslationEvent, MetricSnapshot, TranslationRun,
)
from src.plugins.translate.events import (
TranslationEventLog, VALID_EVENT_TYPES, TERMINAL_EVENT_TYPES,
)
from .conftest import JOB_ID, RUN_ID
class TestLogEvent:
"""Verify log_event method."""
def test_log_invalid_event_type(self, db_session):
"""Negative: unknown event type raises ValueError."""
el = TranslationEventLog(db_session)
with pytest.raises(ValueError, match="Invalid event_type"):
el.log_event(job_id=JOB_ID, event_type="UNKNOWN_TYPE")
def test_log_valid_event_no_run(self, db_session):
"""Happy: job-level event without run_id."""
el = TranslationEventLog(db_session)
event = el.log_event(job_id=JOB_ID, event_type="JOB_CREATED",
created_by="test_user")
assert event.job_id == JOB_ID
assert event.event_type == "JOB_CREATED"
assert event.created_by == "test_user"
def test_log_run_started(self, db_with_run):
"""Happy: RUN_STARTED event for a run."""
session, run_id = db_with_run
el = TranslationEventLog(session)
event = el.log_event(job_id=JOB_ID, run_id=run_id,
event_type="RUN_STARTED")
assert event.run_id == run_id
def test_log_run_event_without_started(self, db_session):
"""Negative: other run events require RUN_STARTED first."""
el = TranslationEventLog(db_session)
run = TranslationRun(id="orphan-run", job_id=JOB_ID, status="RUNNING",
started_at=datetime.now(UTC))
db_session.add(run)
db_session.commit()
with pytest.raises(ValueError, match="RUN_STARTED event must precede"):
el.log_event(job_id=JOB_ID, run_id="orphan-run",
event_type="BATCH_STARTED")
def test_log_terminal_event_twice(self, db_with_run):
"""Negative: cannot log two terminal events for same run."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_COMPLETED")
with pytest.raises(ValueError, match="already has a terminal event"):
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_FAILED")
def test_log_terminal_event_first_time_ok(self, db_with_run):
"""Happy: first terminal event succeeds."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
event = el.log_event(job_id=JOB_ID, run_id=run_id,
event_type="RUN_COMPLETED")
assert event.event_type == "RUN_COMPLETED"
def test_log_with_payload(self, db_with_run):
"""Happy: event with structured payload."""
session, run_id = db_with_run
el = TranslationEventLog(session)
payload = {"rows": 10, "language": "es"}
event = el.log_event(job_id=JOB_ID, run_id=run_id,
event_type="RUN_STARTED", payload=payload)
assert event.event_data == payload
def test_log_event_flushes(self, db_session):
"""Verify flush is called after adding event."""
el = TranslationEventLog(db_session)
event = el.log_event(job_id=JOB_ID, event_type="JOB_CREATED")
fetched = db_session.query(TranslationEvent).filter(
TranslationEvent.id == event.id
).first()
assert fetched is not None
def test_run_started_before_terminal(self, db_with_run):
"""Edge: RUN_STARTED precedes terminal event."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
ev = el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_COMPLETED")
assert ev is not None
class TestQueryEvents:
"""Verify query_events method."""
def test_query_all_events(self, db_session):
"""Happy: query returns all events."""
el = TranslationEventLog(db_session)
el.log_event(job_id=JOB_ID, event_type="JOB_CREATED")
el.log_event(job_id=JOB_ID, event_type="JOB_UPDATED")
results = el.query_events()
assert len(results) >= 1
def test_query_filter_by_job(self, db_session):
"""Happy: filter by job_id."""
el = TranslationEventLog(db_session)
el.log_event(job_id=JOB_ID, event_type="JOB_CREATED")
results = el.query_events(job_id=JOB_ID)
assert len(results) >= 1
assert results[0]["job_id"] == JOB_ID
def test_query_filter_by_run(self, db_with_run):
"""Happy: filter by run_id."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
results = el.query_events(run_id=run_id)
# db_with_run fixture already logs RUN_STARTED during setup, plus this test logs another = 2
assert len(results) == 2
def test_query_filter_by_type(self, db_with_run):
"""Happy: filter by event_type."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, event_type="JOB_CREATED")
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
results = el.query_events(event_type="JOB_CREATED")
assert len(results) == 1
def test_query_limit_offset(self, db_session):
"""Edge: pagination with limit and offset."""
el = TranslationEventLog(db_session)
for i in range(5):
el.log_event(job_id=JOB_ID, event_type="JOB_UPDATED",
payload={"seq": i})
results = el.query_events(limit=2, offset=0)
assert len(results) == 2
results_page2 = el.query_events(limit=2, offset=2)
assert len(results_page2) == 2
class TestPruneExpired:
"""Verify prune_expired method."""
def test_no_expired_events(self, db_session):
"""Edge: no events older than cutoff."""
el = TranslationEventLog(db_session)
el.log_event(job_id=JOB_ID, event_type="JOB_CREATED")
result = el.prune_expired(retention_days=365)
assert result["pruned"] == 0
assert result["snapshot_id"] is None
def test_prune_expired_events(self, db_session):
"""Happy: prune old events and create snapshot."""
el = TranslationEventLog(db_session)
old_event = TranslationEvent(
id="old-event", job_id=JOB_ID, event_type="JOB_CREATED",
event_data={}, created_at=datetime.now(UTC) - timedelta(days=400),
)
db_session.add(old_event)
db_session.commit()
result = el.prune_expired(retention_days=90)
assert result["pruned"] >= 1
assert result["snapshot_id"] is not None
remaining = db_session.query(TranslationEvent).all()
assert len(remaining) == 0
def test_prune_with_metric_snapshot_created(self, db_session):
"""Invariant: MetricSnapshot created before pruning."""
el = TranslationEventLog(db_session)
old_event = TranslationEvent(
id="old-with-metrics", job_id=JOB_ID, event_type="BATCH_COMPLETED",
event_data={"token_count": 500, "cost": 0.01,
"language_code": "es", "run_id": "run-1"},
created_at=datetime.now(UTC) - timedelta(days=400),
)
db_session.add(old_event)
db_session.commit()
result = el.prune_expired(retention_days=90)
assert result["snapshot_id"] is not None
snapshot = db_session.query(MetricSnapshot).first()
assert snapshot is not None
assert snapshot.total_records == 1
def test_prune_language_metrics_aggregation(self, db_session):
"""Edge: per-language metrics aggregated correctly."""
el = TranslationEventLog(db_session)
for i, lang in enumerate(["es", "es", "fr"]):
e = TranslationEvent(
id=f"old-{lang}-{i}", job_id=JOB_ID, event_type="BATCH_COMPLETED",
event_data={"token_count": 100, "cost": 0.01,
"language_code": lang},
created_at=datetime.now(UTC) - timedelta(days=400),
)
db_session.add(e)
db_session.commit()
result = el.prune_expired(retention_days=90)
assert result["pruned"] == 3
snapshot = db_session.query(MetricSnapshot).first()
assert snapshot is not None
metrics = snapshot.per_language_metrics or {}
assert "es" in metrics
assert metrics["es"]["cumulative_tokens"] == 200
assert "fr" in metrics
def test_prune_event_without_language(self, db_session):
"""Edge: event without language_code grouped as _unknown_."""
el = TranslationEventLog(db_session)
e = TranslationEvent(
id="no-lang", job_id=JOB_ID, event_type="BATCH_COMPLETED",
event_data={"token_count": 50},
created_at=datetime.now(UTC) - timedelta(days=400),
)
db_session.add(e)
db_session.commit()
result = el.prune_expired(retention_days=90)
assert result["pruned"] == 1
class TestGetRunEventSummary:
"""Verify get_run_event_summary method."""
def test_valid_run_summary(self, db_with_run):
"""Happy: run with started and completed events."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_COMPLETED")
summary = el.get_run_event_summary(run_id)
assert summary["run_id"] == run_id
assert summary["has_run_started"] is True
assert summary["terminal_event_count"] == 1
assert summary["invariant_valid"] is True
def test_empty_run_summary(self, db_session):
"""Edge: run with no events."""
el = TranslationEventLog(db_session)
summary = el.get_run_event_summary("run-empty")
assert summary["event_count"] == 0
assert summary["has_run_started"] is False
assert summary["invariant_valid"] is False
def test_run_missing_start(self, db_session):
"""Edge: run with terminal but no start."""
el = TranslationEventLog(db_session)
# Need a run record to satisfy FK constraint on translation_events.run_id
run = TranslationRun(id="run-no-start-2", job_id=JOB_ID, status="RUNNING",
started_at=datetime.now(UTC))
db_session.add(run)
db_session.flush()
e = TranslationEvent(
id="orphan-completed", job_id=JOB_ID, run_id="run-no-start-2",
event_type="RUN_COMPLETED", event_data={},
created_at=datetime.now(UTC),
)
db_session.add(e)
db_session.commit()
summary = el.get_run_event_summary("run-no-start-2")
assert summary["has_run_started"] is False
assert summary["invariant_valid"] is False
def test_run_two_terminals_violation(self, db_with_run):
"""Edge: run with two terminal events violates invariant."""
session, run_id = db_with_run
el = TranslationEventLog(session)
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_STARTED")
el.log_event(job_id=JOB_ID, run_id=run_id, event_type="RUN_COMPLETED")
# Force-add a second terminal
e2 = TranslationEvent(
id="second-terminal", job_id=JOB_ID, run_id=run_id,
event_type="RUN_FAILED", event_data={},
created_at=datetime.now(UTC),
)
session.add(e2)
session.commit()
session.expire_all()
summary = el.get_run_event_summary(run_id)
assert summary["terminal_event_count"] > 1
assert summary["invariant_valid"] is False
# #endregion Test.TranslationEventLog