合并两个不可分割的深化: Phase 2 — 智能作业结算统一(ADR-0012) - intelligence_jobs.execute(job_kind, campaign_id, ...) 作为结算的 唯一实现:建行 → 认领 → 校验 → generating → 落账,一处编排、 一处截断(500 字符)。两个 executor 退化为 ensure_queued / validate / work_fn 三个小 adapter。 - analysis.validate_analysis_request() 共享校验入口(活动终态 → 模型),路由捕获映射 400、executor 捕获落 failed 行,与 validate_comparison_request 先例同构。 - campaign_runner._auto_start_analysis 的跳过守卫收敛至 auto_intelligence_eligible 单一判断点。 - comparison.py 删除零调用的 build_comparison_payload; load_comparison_view 投影归位至 campaign_read_model。 - 新增 characterization 测试(认领竞争、重复触发、截断、恢复上限)。 Phase 3 — storage/repository.py 拆分 - AsyncJobRepository 及两个子类迁至 storage/async_job_repository.py(Phase 2 的 intelligence_jobs 与 comparison 必须 import 自该路径,故与 Phase 2 同 commit)。 - ExplorationSession / ExplorationMessage 迁至 storage/exploration_repository.py;repository.py 由 1180 行降至 约 814 行,grep 确认无残留符号。 - exploration 子模块与路由 import 全部更新;测试 import 跟随。 刻意不做:CAS 共享原语、app.py 五 registry 关停顺序归一 (ADR-0006 精神,等真实需求出现再议)。
178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
"""周期对比读模型与校验合一直测(架构保养第二轮候选 3)。
|
||
|
||
load_comparison_view 单一出口返回 GET /comparison 完整响应形状;
|
||
validate_comparison_request 共享校验入口抛 ComparisonError,router
|
||
映射 400、执行器落 failed 行。
|
||
"""
|
||
|
||
from datetime import datetime, timezone
|
||
from uuid import uuid4
|
||
|
||
import pytest
|
||
from agenteval.evaluation.campaign_read_model import load_comparison_view
|
||
from agenteval.evaluation.comparison import (
|
||
ComparisonError,
|
||
validate_comparison_request,
|
||
)
|
||
from agenteval.models import (
|
||
Campaign,
|
||
CampaignPlanEntry,
|
||
CampaignStatus,
|
||
Case,
|
||
CaseType,
|
||
EvalTarget,
|
||
Scenario,
|
||
)
|
||
from agenteval.storage.async_job_repository import CampaignAnalysisRepository, CampaignPeriodComparisonRepository
|
||
from agenteval.storage.repository import (
|
||
CampaignRepository,
|
||
ScenarioRepository,
|
||
TargetRepository,
|
||
)
|
||
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
|
||
CampaignAnalysisDB,
|
||
CampaignDB,
|
||
CampaignPeriodComparisonDB,
|
||
EvalRunDB,
|
||
EvalTargetDB,
|
||
ScenarioDB,
|
||
)
|
||
|
||
engine = create_engine(
|
||
f"sqlite:///{tmp_path / 'comparison.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_campaign(db_session, *, status=CampaignStatus.COMPLETED):
|
||
target = TargetRepository(db_session).create(
|
||
EvalTarget(id=f"t-{uuid4().hex[:8]}", name="数字员工")
|
||
)
|
||
scenario = ScenarioRepository(db_session).create(
|
||
Scenario(id=f"s-{uuid4().hex[:8]}", name="查账单", target_id=target.id, cases=[Case(id="c-1", type=CaseType.SINGLE, messages=["你好"])])
|
||
)
|
||
campaign = CampaignRepository(db_session).create(
|
||
Campaign(
|
||
name="cycle",
|
||
target_id=target.id,
|
||
window_seconds=3600,
|
||
time_scale=1.0,
|
||
plan=[CampaignPlanEntry(scenario_id=scenario.id, offset_seconds=0, count=1)],
|
||
status=status,
|
||
started_at=T0,
|
||
)
|
||
)
|
||
return campaign
|
||
|
||
|
||
def test_load_comparison_view_no_row_returns_none_status(db_session):
|
||
campaign = _seed_campaign(db_session)
|
||
view = load_comparison_view(db_session, campaign)
|
||
assert view["status"] == "none"
|
||
assert view["comparison"] is None
|
||
assert "auto_baseline" in view
|
||
assert "metric_diff" in view
|
||
|
||
|
||
def test_load_comparison_view_generating_row(db_session):
|
||
campaign = _seed_campaign(db_session)
|
||
CampaignPeriodComparisonRepository(db_session).upsert(
|
||
campaign.id,
|
||
status="generating",
|
||
baseline_campaign_id="baseline-1",
|
||
triggered_by="manual",
|
||
)
|
||
view = load_comparison_view(db_session, campaign)
|
||
assert view["status"] == "generating"
|
||
assert view["comparison"] is not None
|
||
assert view["comparison"]["baseline_campaign_id"] == "baseline-1"
|
||
|
||
|
||
def test_load_comparison_view_completed_row_includes_model_name(db_session):
|
||
campaign = _seed_campaign(db_session)
|
||
CampaignPeriodComparisonRepository(db_session).upsert(
|
||
campaign.id,
|
||
status="completed",
|
||
baseline_campaign_id="baseline-1",
|
||
result={"trend": "improving"},
|
||
model_config_id="mc-1",
|
||
triggered_by="manual",
|
||
)
|
||
view = load_comparison_view(db_session, campaign)
|
||
assert view["status"] == "completed"
|
||
assert view["comparison"]["result"] == {"trend": "improving"}
|
||
assert "model_name" in view["comparison"]
|
||
|
||
|
||
def test_validate_rejects_non_completed_campaign(db_session):
|
||
campaign = _seed_campaign(db_session, status=CampaignStatus.RUNNING)
|
||
with pytest.raises(ComparisonError, match="活动完成后"):
|
||
validate_comparison_request(db_session, campaign)
|
||
|
||
|
||
def test_validate_rejects_missing_analysis_model(db_session, monkeypatch):
|
||
campaign = _seed_campaign(db_session)
|
||
monkeypatch.setattr(
|
||
"agenteval.evaluation.comparison.resolve_analysis_model",
|
||
lambda *a, **kw: None,
|
||
)
|
||
with pytest.raises(ComparisonError, match="未配置分析模型"):
|
||
validate_comparison_request(db_session, campaign)
|
||
|
||
|
||
def test_validate_rejects_incomplete_current_analysis(db_session, monkeypatch):
|
||
campaign = _seed_campaign(db_session)
|
||
monkeypatch.setattr(
|
||
"agenteval.evaluation.comparison.resolve_analysis_model",
|
||
lambda *a, **kw: object(),
|
||
)
|
||
CampaignAnalysisRepository(db_session).upsert(
|
||
campaign.id, status="generating", triggered_by="auto"
|
||
)
|
||
with pytest.raises(ComparisonError, match="请先生成本期活动的智能分析"):
|
||
validate_comparison_request(db_session, campaign)
|
||
|
||
|
||
def test_validate_rejects_missing_explicit_baseline(db_session, monkeypatch):
|
||
campaign = _seed_campaign(db_session)
|
||
monkeypatch.setattr(
|
||
"agenteval.evaluation.comparison.resolve_analysis_model",
|
||
lambda *a, **kw: object(),
|
||
)
|
||
CampaignAnalysisRepository(db_session).upsert(
|
||
campaign.id, status="completed", result={}, triggered_by="auto"
|
||
)
|
||
with pytest.raises(ComparisonError, match="基线活动不存在"):
|
||
validate_comparison_request(db_session, campaign, explicit_baseline_id="missing")
|
||
|
||
|
||
def test_validate_rejects_incomplete_baseline_analysis(db_session, monkeypatch):
|
||
campaign = _seed_campaign(db_session)
|
||
monkeypatch.setattr(
|
||
"agenteval.evaluation.comparison.resolve_analysis_model",
|
||
lambda *a, **kw: object(),
|
||
)
|
||
baseline = _seed_campaign(db_session)
|
||
CampaignAnalysisRepository(db_session).upsert(
|
||
campaign.id, status="completed", result={}, triggered_by="auto"
|
||
)
|
||
CampaignAnalysisRepository(db_session).upsert(
|
||
baseline.id, status="generating", triggered_by="auto"
|
||
)
|
||
with pytest.raises(ComparisonError, match="基线活动没有已完成的智能分析"):
|
||
validate_comparison_request(db_session, campaign, explicit_baseline_id=baseline.id)
|