"""活动报告读模型 load_campaign_report 与 ScenarioRepository.name_map 直测。 读模型是报告 / 分析 / 对比 / 导出共用的「取数 + 聚合」单一出口: 返回结果必须与手写三件套(list_by_campaign + scenario_names + generate_campaign_report)完全一致。 """ from datetime import datetime, timedelta, timezone import pytest from agenteval.evaluation.report import generate_campaign_report, load_campaign_report from agenteval.models import Campaign, CampaignPlanEntry, CampaignStatus, Case, CaseType, EvalRun, RunStatus, Scenario from agenteval.storage.repository import CampaignRepository, RunRepository, ScenarioRepository from sqlmodel import Session, SQLModel, create_engine T0 = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc) @pytest.fixture() def db_session(tmp_path): from agenteval.storage.db import ( # noqa: F401 CampaignDB, EvalResultDB, EvalRunDB, EvalTargetDB, FileCategoryDB, FileRecordDB, ScenarioDB, TurnDB, ) engine = create_engine( f"sqlite:///{tmp_path / 'loader.db'}", connect_args={"check_same_thread": False}, ) SQLModel.metadata.create_all(engine) session = Session(engine) try: yield session finally: session.close() engine.dispose() def _seed(session) -> Campaign: ScenarioRepository(session).create(Scenario( id="s-a", name="夜间问诊", cases=[Case(id="c1", type=CaseType.SINGLE, messages=["hi"])], )) campaign = CampaignRepository(session).create(Campaign( name="cycle", target_id="t-1", window_seconds=12, time_scale=1.0, plan=[CampaignPlanEntry(scenario_id="s-a", offset_seconds=0, count=1)], )) campaign.status = CampaignStatus.RUNNING campaign.started_at = T0 campaign = CampaignRepository(session).update(campaign) RunRepository(session).create(EvalRun( target_id="t-1", scenario_id="s-a", campaign_id=campaign.id, status=RunStatus.COMPLETED, started_at=T0 + timedelta(seconds=1), summary={"total_cases": 1, "passed_cases": 1, "pass_rate": 1.0, "avg_latency_ms": 100}, )) return campaign def test_load_campaign_report_matches_manual_trio(db_session): campaign = _seed(db_session) report = load_campaign_report(db_session, campaign) runs = RunRepository(db_session).list_by_campaign(campaign.id) scenario_names = {s.id: s.name for s in ScenarioRepository(db_session).list_all()} expected = generate_campaign_report(campaign, runs, scenario_names=scenario_names) assert report == expected cap = next(c for c in report["capability_summary"] if c["scenario_id"] == "s-a") assert cap["scenario_name"] == "夜间问诊" def test_load_campaign_report_empty_campaign(db_session): campaign = CampaignRepository(db_session).create(Campaign( name="empty", target_id="t-1", window_seconds=12, plan=[CampaignPlanEntry(scenario_id="s-x", offset_seconds=0, count=1)], )) report = load_campaign_report(db_session, campaign) assert report["summary"]["total_runs"] == 0 assert report["capability_summary"] == [] def test_scenario_name_map(db_session): _seed(db_session) ScenarioRepository(db_session).create(Scenario( id="s-b", name="缴费引导", cases=[Case(id="c2", type=CaseType.SINGLE, messages=["缴费"])], )) assert ScenarioRepository(db_session).name_map() == {"s-a": "夜间问诊", "s-b": "缴费引导"} def test_scenario_name_map_empty(db_session): assert ScenarioRepository(db_session).name_map() == {}