AgentEvalTool/tests/unit/test_repository.py
sinohqb d411572607 refactor(storage): converge CRUD skeleton into BaseRepository
Target/Scenario/Run/Campaign repositories repeated the same
__init__/list_all/get/create/delete skeleton (~120 lines). Extract a
generic BaseRepository[M, DB]: subclasses declare the table + ordering
column and implement instance-method _to_db/_from_db converters (so
Scenario's _from_db can reach self.session for model bindings). Bespoke
paths (Scenario create/delete, all update) stay per-subclass. Adds
test_repository.py locking the CRUD + JSON round-trip contract.
2026-07-31 14:30:11 +08:00

114 lines
3.3 KiB
Python

"""CRUD + serialization round-trip characterization for the repository layer.
These lock the create → get → list_all → delete contract shared by the
Target / Run / Campaign repositories (the BaseRepository skeleton), plus the
summary/plan JSON round-trip, so the generic-base refactor stays behaviour-
preserving. Scenario's bespoke create/update (binding validation, versioning)
is covered by its own tests.
"""
from datetime import datetime, timezone
from agenteval.models import (
Campaign,
CampaignPlanEntry,
ChannelType,
EvalRun,
EvalTarget,
PlatformType,
RunStatus,
RunSummary,
TargetStatus,
)
from agenteval.storage.repository import (
CampaignRepository,
RunRepository,
TargetRepository,
)
def _make_target(tid: str = "t-1") -> EvalTarget:
return EvalTarget(
id=tid,
name="target",
platform=PlatformType.AI_DIGITAL_EMPLOYEE,
channel_type=ChannelType.TUTU_API,
channel_config={
"base_url": "x",
"token": "x",
"tenant": "x",
"chat_channel_id": "x",
"chat_contact_id": "x",
},
)
def test_target_crud_round_trip(db_session):
repo = TargetRepository(db_session)
repo.create(_make_target())
fetched = repo.get("t-1")
assert fetched is not None
assert fetched.name == "target"
assert fetched.channel_config["base_url"] == "x"
assert fetched.status == TargetStatus.ACTIVE or fetched.status is not None
assert [t.id for t in repo.list_all()] == ["t-1"]
assert repo.delete("t-1") is True
assert repo.get("t-1") is None
assert repo.delete("t-1") is False
def test_target_list_all_newest_first(db_session):
repo = TargetRepository(db_session)
older = _make_target("t-old")
older.created_at = datetime(2026, 1, 1, tzinfo=timezone.utc)
newer = _make_target("t-new")
newer.created_at = datetime(2026, 6, 1, tzinfo=timezone.utc)
repo.create(older)
repo.create(newer)
assert [t.id for t in repo.list_all()] == ["t-new", "t-old"]
def test_run_summary_json_round_trip(db_session):
TargetRepository(db_session).create(_make_target())
repo = RunRepository(db_session)
repo.create(
EvalRun(
id="r-1",
target_id="t-1",
scenario_id="s-1",
status=RunStatus.COMPLETED,
summary=RunSummary(total_cases=2, passed_cases=1, pass_rate=0.5),
)
)
fetched = repo.get("r-1")
assert fetched is not None
assert fetched.summary is not None
assert fetched.summary.total_cases == 2
assert fetched.summary.pass_rate == 0.5
def test_campaign_crud_and_plan_round_trip(db_session):
TargetRepository(db_session).create(_make_target())
repo = CampaignRepository(db_session)
repo.create(
Campaign(
id="cp-1",
name="campaign",
target_id="t-1",
window_seconds=3600,
plan=[CampaignPlanEntry(scenario_id="s-1", offset_seconds=0, count=2)],
)
)
fetched = repo.get("cp-1")
assert fetched is not None
assert fetched.plan[0].scenario_id == "s-1"
assert fetched.plan[0].count == 2
assert [c.id for c in repo.list_all()] == ["cp-1"]
# Campaign inherits the shared delete() from the base repository.
assert repo.delete("cp-1") is True
assert repo.get("cp-1") is None