"""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, Turn, ) from agenteval.storage.repository import ( CampaignRepository, ResultRepository, 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_child_identity_round_trip(db_session): """Campaign plan identity survives the repository's model conversion seam.""" TargetRepository(db_session).create(_make_target()) repo = RunRepository(db_session) repo.create( EvalRun( id="child-1", target_id="t-1", scenario_id="s-1", campaign_id="campaign-1", campaign_plan_index=2, campaign_occurrence_index=1, ) ) fetched = repo.get("child-1") assert fetched is not None assert fetched.campaign_id == "campaign-1" assert fetched.campaign_plan_index == 2 assert fetched.campaign_occurrence_index == 1 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 def test_update_turn_exchange_preserves_sent_fact(db_session): TargetRepository(db_session).create(_make_target()) RunRepository(db_session).create(EvalRun(id="r-1", target_id="t-1", scenario_id="s-1", status=RunStatus.RUNNING)) repo = ResultRepository(db_session) repo.save_turn( Turn( id="turn-1", run_id="r-1", case_id="case-1", round_index=1, sent_message={"msgType": "text", "msgBody": {"content": "hello"}}, sent_at=datetime(2026, 8, 6, 1, 0, tzinfo=timezone.utc), ) ) updated = repo.update_turn_exchange( "turn-1", question_msg_id="question-1", reply={"msgBody": {"content": "world"}}, received_at=datetime(2026, 8, 6, 1, 0, 1, tzinfo=timezone.utc), latency_ms=1000, ) assert updated is not None assert updated.get_sent_message() == {"msgType": "text", "msgBody": {"content": "hello"}} assert updated.case_id == "case-1" assert updated.round_index == 1 assert updated.question_msg_id == "question-1" assert updated.get_reply() == {"msgBody": {"content": "world"}} assert updated.latency_ms == 1000 def test_update_turn_exchange_returns_none_for_unknown_turn(db_session): assert ( ResultRepository(db_session).update_turn_exchange( "missing", question_msg_id="question-1", reply=None, received_at=None, latency_ms=None, ) is None )