# #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