"""Startup cleanup of orphan runs (interrupted by server restart).""" from agenteval.models import Campaign, CampaignPlanEntry, CampaignStatus, EvalRun, RunStatus from agenteval.storage.repository import CampaignRepository, RunRepository def _make_run(session, status: RunStatus) -> str: run = RunRepository(session).create(EvalRun( target_id="t-1", scenario_id="s-1", status=status, )) return run.id def test_mark_orphans_failed(db_session): repo = RunRepository(db_session) running_id = _make_run(db_session, RunStatus.RUNNING) pending_id = _make_run(db_session, RunStatus.PENDING) completed_id = _make_run(db_session, RunStatus.COMPLETED) failed_id = _make_run(db_session, RunStatus.FAILED) count = repo.mark_orphans_failed() assert count == 2 for rid in (running_id, pending_id): run = repo.get(rid) assert run.status == RunStatus.FAILED assert run.summary.error.code == "interrupted" assert run.completed_at is not None # 已完结的运行不受影响 assert repo.get(completed_id).status == RunStatus.COMPLETED assert repo.get(failed_id).status == RunStatus.FAILED assert repo.get(completed_id).summary is None def test_mark_orphans_failed_noop_when_clean(db_session): repo = RunRepository(db_session) _make_run(db_session, RunStatus.COMPLETED) assert repo.mark_orphans_failed() == 0 def test_mark_orphans_preserves_only_recoverable_campaign_pending(db_session): campaign = CampaignRepository(db_session).create( Campaign( id="campaign-1", name="running", target_id="t-1", status=CampaignStatus.RUNNING, window_seconds=60, plan=[CampaignPlanEntry(scenario_id="s-1", offset_seconds=0, count=2)], ) ) repo = RunRepository(db_session) pending = repo.create( EvalRun( target_id="t-1", scenario_id="s-1", campaign_id=campaign.id, campaign_plan_index=0, campaign_occurrence_index=0, status=RunStatus.PENDING, ) ) running = repo.create( EvalRun( target_id="t-1", scenario_id="s-1", campaign_id=campaign.id, campaign_plan_index=0, campaign_occurrence_index=1, status=RunStatus.RUNNING, ) ) count = repo.mark_orphans_failed() assert count == 1 assert repo.get(pending.id).status is RunStatus.PENDING assert repo.get(running.id).status is RunStatus.FAILED def test_update_preserves_scenario_version_and_triggered_by(db_session): """update() 不得丢字段:scenario_version / triggered_by 必须回写(漂移回归)。""" from agenteval.models import RunTrigger repo = RunRepository(db_session) run = repo.create(EvalRun( target_id="t-1", scenario_id="s-1", status=RunStatus.RUNNING, scenario_version=4, triggered_by=RunTrigger.AI_ASSISTANT, )) run.scenario_version = 5 run.triggered_by = RunTrigger.CLI run.status = RunStatus.COMPLETED updated = repo.update(run) assert updated.scenario_version == 5 assert updated.triggered_by == RunTrigger.CLI