"""Tests for idempotent Campaign completion and settlement ordering.""" import pytest from agenteval.evaluation.campaign_lifecycle import CampaignLifecycleError, complete_campaign from agenteval.exploration.models import ExplorationSession, ExplorationSessionStatus from agenteval.models import Campaign, CampaignPlanEntry, CampaignStatus from agenteval.storage.exploration_repository import ExplorationSessionRepository from agenteval.storage.repository import CampaignRepository from sqlalchemy import event def _campaign(session, status=CampaignStatus.RUNNING): return CampaignRepository(session).create( Campaign( id="campaign-1", name="campaign", target_id="t-1", status=status, window_seconds=60, plan=[CampaignPlanEntry(scenario_id="s-1", offset_seconds=0, count=1)], ) ) def test_complete_atomically_settles_running_exploration(db_session): _campaign(db_session) session_obj = ExplorationSessionRepository(db_session).create( ExplorationSession(campaign_id="campaign-1", target_id="t-1") ) completed = complete_campaign(db_session, "campaign-1") assert completed.status is CampaignStatus.COMPLETED assert completed.completed_at is not None assert ExplorationSessionRepository(db_session).get(session_obj.id).status is ExplorationSessionStatus.EXPIRED def test_complete_is_idempotency_guarded(db_session): _campaign(db_session) complete_campaign(db_session, "campaign-1") with pytest.raises(CampaignLifecycleError) as again: complete_campaign(db_session, "campaign-1") assert again.value.status_code == 409 def test_cancel_wins_completion_race(db_session): _campaign(db_session, CampaignStatus.CANCELLED) with pytest.raises(CampaignLifecycleError) as conflict: complete_campaign(db_session, "campaign-1") assert conflict.value.status_code == 409 def test_settlement_failure_keeps_campaign_running_and_session_open(db_session): _campaign(db_session) session_obj = ExplorationSessionRepository(db_session).create( ExplorationSession(campaign_id="campaign-1", target_id="t-1") ) engine = db_session.get_bind() def fail_exploration_update(_connection, _cursor, statement, _parameters, _context, _executemany): if statement.lstrip().upper().startswith("UPDATE EXPLORATION_SESSIONS"): raise RuntimeError("settlement failed") event.listen(engine, "before_cursor_execute", fail_exploration_update) try: with pytest.raises(RuntimeError, match="settlement failed"): complete_campaign(db_session, "campaign-1") finally: event.remove(engine, "before_cursor_execute", fail_exploration_update) db_session.rollback() assert CampaignRepository(db_session).get("campaign-1").status is CampaignStatus.RUNNING assert ExplorationSessionRepository(db_session).get(session_obj.id).status is ExplorationSessionStatus.RUNNING