合并两个不可分割的深化: 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 精神,等真实需求出现再议)。
74 lines
2.9 KiB
Python
74 lines
2.9 KiB
Python
"""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
|