AgentEvalTool/tests/unit/test_campaign_narrow_updates.py
sinohqb 38849d46f1 refactor(repository): narrow atomic updates for patrol/cancel/scheduler writes
全行 update 逼着调用方承诺「持有最新完整快照」,巡检水位、活动取消、
调度循环三方并发写入会互相覆盖(后写者抹掉先写者的 summary/status)。
CampaignRepository 新增三个窄口径原子操作:touch_patrol_watermark、
mark_cancelled、save_scheduler_state,各自只写自己的列;三处调用点迁移。
2026-08-04 03:39:42 +08:00

103 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""CampaignRepository 窄口径原子更新直测(架构保养候选 1
全行 update 逼着调用方承诺「持有最新完整快照」,巡检水位 / 取消 /
调度循环并发写入会互相覆盖。三个窄方法各自只动自己的列:
touch_patrol_watermark水位、mark_cancelled终态迁移
save_scheduler_state调度进度
"""
from datetime import datetime, timezone
import pytest
from agenteval.models import Campaign, CampaignPlanEntry, CampaignStatus, CampaignSummary, SchedulerState
from agenteval.storage.db import as_utc
from agenteval.storage.repository import CampaignRepository
from sqlmodel import Session, SQLModel, create_engine
T0 = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
T1 = datetime(2026, 1, 1, 1, 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 / 'narrow.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(db_session, status=CampaignStatus.RUNNING) -> Campaign:
campaign = CampaignRepository(db_session).create(Campaign(
name="cycle", target_id="t-1", window_seconds=3600,
plan=[CampaignPlanEntry(scenario_id="s-a", offset_seconds=0, count=1)],
status=status, started_at=T0,
summary=CampaignSummary(scheduler=SchedulerState(spawned_indices=[0])),
))
return campaign
def test_touch_patrol_watermark_only_touches_watermark(db_session):
campaign = _seed(db_session)
CampaignRepository(db_session).touch_patrol_watermark(campaign.id, T1)
fresh = CampaignRepository(db_session).get(campaign.id)
assert as_utc(fresh.last_patrolled_at) == T1
# 其余列原样:窄口径不覆写并发写入的 status / summary
assert fresh.status == CampaignStatus.RUNNING
assert fresh.summary.scheduler.spawned_indices == [0]
assert fresh.name == "cycle" and as_utc(fresh.started_at) == T0
def test_mark_cancelled_only_touches_status_and_completed_at(db_session):
campaign = _seed(db_session)
CampaignRepository(db_session).touch_patrol_watermark(campaign.id, T1)
updated = CampaignRepository(db_session).mark_cancelled(campaign.id, T1)
assert updated.status == CampaignStatus.CANCELLED
assert as_utc(updated.completed_at) == T1
fresh = CampaignRepository(db_session).get(campaign.id)
assert fresh.status == CampaignStatus.CANCELLED
assert as_utc(fresh.completed_at) == T1
# 水位与调度进度不被终态迁移抹掉
assert as_utc(fresh.last_patrolled_at) == T1
assert fresh.summary.scheduler.spawned_indices == [0]
def test_save_scheduler_state_only_touches_summary(db_session):
campaign = _seed(db_session)
CampaignRepository(db_session).touch_patrol_watermark(campaign.id, T1)
new_summary = CampaignSummary(scheduler=SchedulerState(spawned_indices=[0, 1, 2]))
CampaignRepository(db_session).save_scheduler_state(campaign.id, new_summary)
fresh = CampaignRepository(db_session).get(campaign.id)
assert fresh.summary.scheduler.spawned_indices == [0, 1, 2]
# 状态与水位不被调度进度覆写
assert fresh.status == CampaignStatus.RUNNING
assert as_utc(fresh.last_patrolled_at) == T1
def test_narrow_updates_ignore_missing_campaign(db_session):
repo = CampaignRepository(db_session)
repo.touch_patrol_watermark("missing", T1)
repo.save_scheduler_state("missing", CampaignSummary())
assert repo.mark_cancelled("missing", T1) is None