"""Integration tests for the runner's auto analysis hook (v0.7 ticket 05). Only 正式线 campaigns (time_scale == 1) enqueue the analysis task on COMPLETED; accelerated/cancelled campaigns and missing analysis models all skip silently. The analysis service itself is spied, not executed. """ import pytest from agenteval.evaluation import campaign_runner from agenteval.evaluation.campaign_runner import request_cancel, start_campaign from agenteval.models import ( Campaign, CampaignPlanEntry, CampaignStatus, Case, CaseType, ChannelType, EvalTarget, PlatformType, Scenario, TargetStatus, ) from agenteval.storage.db import utc_now from agenteval.storage.repository import CampaignRepository, ScenarioRepository, TargetRepository from tests.unit.mock_channel import MockChannel TICK = 0.01 @pytest.fixture() def seeded_db(db_session, monkeypatch): from agenteval.channels import factory as factory_module from agenteval.evaluation import engine as engine_module from agenteval.storage import db as db_module from agenteval.storage import repository as repo_module def _test_get_session(): return db_session monkeypatch.setattr(db_module, "get_session", _test_get_session) monkeypatch.setattr(repo_module, "get_session", _test_get_session) monkeypatch.setattr(engine_module, "get_session", _test_get_session) monkeypatch.setattr(campaign_runner, "get_session", _test_get_session) channel = MockChannel(reply_delay=0.0) monkeypatch.setattr(factory_module.ChannelFactory, "create", lambda target: channel) TargetRepository(db_session).create(EvalTarget( id="t-1", name="mock-target", platform=PlatformType.AI_DIGITAL_EMPLOYEE, channel_type=ChannelType.TUTU_API, channel_config={"base_url": "http://mock", "token": "x"}, status=TargetStatus.ACTIVE, )) ScenarioRepository(db_session).create(Scenario( id="s-1", name="mock-scenario", cases=[Case(id="c1", type=CaseType.SINGLE, messages=["hi"])], )) return db_session @pytest.fixture() def analysis_spy(monkeypatch): """Spy the analysis seam: resolvable model, recorded enqueue calls.""" calls: list[tuple[str, str]] = [] monkeypatch.setattr( campaign_runner, "start_campaign_analysis", lambda cid, *, triggered_by: calls.append((cid, triggered_by)), ) monkeypatch.setattr(campaign_runner, "resolve_analysis_model", lambda campaign, session: object()) return calls def _make_campaign(session, **overrides) -> Campaign: payload = dict( name="auto-trigger", target_id="t-1", window_seconds=1, time_scale=1.0, # 正式线:1s 窗口真实耗时 ~1s plan=[CampaignPlanEntry(scenario_id="s-1", offset_seconds=0, count=1)], ) payload.update(overrides) return CampaignRepository(session).create(Campaign(**payload)) async def _await_task(campaign_id, timeout=5.0): import asyncio task = campaign_runner.campaign_registry.get(campaign_id) if task is not None: await asyncio.wait_for(task, timeout=timeout) async def test_realtime_completion_auto_enqueues_analysis(seeded_db, analysis_spy): campaign = _make_campaign(seeded_db) start_campaign(campaign.id, seeded_db, tick_seconds=TICK) await _await_task(campaign.id) final = CampaignRepository(seeded_db).get(campaign.id) assert final.status == CampaignStatus.COMPLETED assert analysis_spy == [(campaign.id, "auto")] async def test_accelerated_completion_does_not_enqueue(seeded_db, analysis_spy): campaign = _make_campaign(seeded_db, time_scale=1000.0) # 加速调试线 start_campaign(campaign.id, seeded_db, tick_seconds=TICK) await _await_task(campaign.id) final = CampaignRepository(seeded_db).get(campaign.id) assert final.status == CampaignStatus.COMPLETED assert analysis_spy == [] async def test_cancelled_campaign_does_not_enqueue(seeded_db, analysis_spy): import asyncio campaign = _make_campaign(seeded_db, window_seconds=100) start_campaign(campaign.id, seeded_db, tick_seconds=TICK) await asyncio.sleep(0.05) repo = CampaignRepository(seeded_db) current = repo.get(campaign.id) current.status = CampaignStatus.CANCELLED current.completed_at = utc_now() repo.update(current) request_cancel(campaign.id) await _await_task(campaign.id) assert repo.get(campaign.id).status == CampaignStatus.CANCELLED assert analysis_spy == [] async def test_missing_analysis_model_skips_silently(seeded_db, monkeypatch, analysis_spy): monkeypatch.setattr(campaign_runner, "resolve_analysis_model", lambda campaign, session: None) campaign = _make_campaign(seeded_db) start_campaign(campaign.id, seeded_db, tick_seconds=TICK) await _await_task(campaign.id) final = CampaignRepository(seeded_db).get(campaign.id) assert final.status == CampaignStatus.COMPLETED # 活动完成流程不受影响 assert analysis_spy == []