"""Characterization tests locking the intelligent-eval state machine. Locks the ``_TRANSITIONS`` table through the public lifecycle API before the module is split into phase sub-modules, so the split cannot silently change which transitions are allowed: draft → planning → pending_approval → executing → completed → cancelled → failed pending_approval → planning (reject, with feedback) pending_approval / executing → cancelled completed / cancelled / failed → deleted """ import json import pytest from agenteval.intelligent_eval import lifecycle from agenteval.intelligent_eval.models import IntelligentEvalSessionStatus, IntelligentEvalStatus from agenteval.models import ChannelType, EvalTarget, PlatformType, TargetStatus from agenteval.storage.db import IntelligentEvalSessionDB, utc_now from agenteval.storage.repository import TargetRepository from sqlmodel import Session def _seed_target(session: Session, target_id: str = "t-1") -> None: TargetRepository(session).create( EvalTarget( id=target_id, name="ie-target", platform=PlatformType.AI_DIGITAL_EMPLOYEE, channel_type=ChannelType.TUTU_API, channel_config={ "base_url": "http://mock", "token": "x", "tenant": "t", "chat_channel_id": "c", "chat_contact_id": "u", }, status=TargetStatus.ACTIVE, ) ) def _create(session: Session) -> str: ev = lifecycle.create_eval( session, name="char-eval", target_id="t-1", goal="评估服务质量", seeds={}, intent="关注售后", role_description="客服数字员工", ) return ev.id def _to_pending(session: Session) -> str: eval_id = _create(session) lifecycle.submit_plan(session, eval_id, {"estimated_sessions": 2}) return eval_id def _to_executing(session: Session) -> str: eval_id = _to_pending(session) lifecycle.approve(session, eval_id) return eval_id @pytest.fixture() def seeded(db_session): _seed_target(db_session) return db_session def test_create_enters_planning(seeded): ev = lifecycle.get_eval(seeded, _create(seeded)) assert ev.status is IntelligentEvalStatus.PLANNING def test_happy_path_to_completed(seeded): eval_id = _to_executing(seeded) ev = lifecycle.submit_report(seeded, eval_id, {"summary": "整体良好"}) assert ev.status is IntelligentEvalStatus.COMPLETED def test_submit_plan_moves_to_pending_approval(seeded): ev = lifecycle.submit_plan(seeded, _create(seeded), {"estimated_sessions": 1}) assert ev.status is IntelligentEvalStatus.PENDING_APPROVAL def test_reject_returns_to_planning_with_feedback(seeded): eval_id = _to_pending(seeded) ev = lifecycle.reject(seeded, eval_id, "计划太粗,补充时段分布") assert ev.status is IntelligentEvalStatus.PLANNING assert ev.plan_feedback == "计划太粗,补充时段分布" def test_cancel_from_pending_approval(seeded): eval_id = _to_pending(seeded) assert lifecycle.cancel(seeded, eval_id).status is IntelligentEvalStatus.CANCELLED def test_cancel_from_executing(seeded): eval_id = _to_executing(seeded) assert lifecycle.cancel(seeded, eval_id).status is IntelligentEvalStatus.CANCELLED @pytest.mark.parametrize( "make_state,action", [ (_create, lambda s, i: lifecycle.approve(s, i)), # planning → executing 非法 (_to_executing, lambda s, i: lifecycle.submit_plan(s, i, {})), # executing → planning 非法 (_to_pending, lambda s, i: lifecycle.submit_report(s, i, {})), # pending → completed 非法 ], ids=["approve-during-planning", "submit-plan-during-executing", "report-during-pending"], ) def test_illegal_transitions_raise(seeded, make_state, action): eval_id = make_state(seeded) with pytest.raises(lifecycle.IntelligentEvalTransitionError): action(seeded, eval_id) def test_cancel_rejected_in_planning_and_completed(seeded): planning_id = _create(seeded) with pytest.raises(lifecycle.IntelligentEvalTransitionError): lifecycle.cancel(seeded, planning_id) completed_id = _to_executing(seeded) lifecycle.submit_report(seeded, completed_id, {}) with pytest.raises(lifecycle.IntelligentEvalTransitionError): lifecycle.cancel(seeded, completed_id) def test_delete_from_each_terminal_state(seeded): # completed completed_id = _to_executing(seeded) lifecycle.submit_report(seeded, completed_id, {}) assert lifecycle.delete_eval(seeded, completed_id).status is IntelligentEvalStatus.DELETED # cancelled cancelled_id = _to_pending(seeded) lifecycle.cancel(seeded, cancelled_id) assert lifecycle.delete_eval(seeded, cancelled_id).status is IntelligentEvalStatus.DELETED def test_delete_rejected_while_executing(seeded): eval_id = _to_executing(seeded) with pytest.raises(lifecycle.IntelligentEvalTransitionError): lifecycle.delete_eval(seeded, eval_id) def test_delete_is_idempotent(seeded): completed_id = _to_executing(seeded) lifecycle.submit_report(seeded, completed_id, {}) lifecycle.delete_eval(seeded, completed_id) assert lifecycle.delete_eval(seeded, completed_id).status is IntelligentEvalStatus.DELETED def test_submit_report_blocked_by_running_session(seeded): eval_id = _to_executing(seeded) seeded.add( IntelligentEvalSessionDB( eval_id=eval_id, target_id="t-1", persona=json.dumps({"name": "老客户"}), goal="退货", status=IntelligentEvalSessionStatus.RUNNING.value, created_at=utc_now(), ) ) seeded.commit() with pytest.raises(lifecycle.IntelligentEvalTransitionError, match="进行中的会话"): lifecycle.submit_report(seeded, eval_id, {"summary": "x"}) def test_missing_eval_raises_not_found(seeded): with pytest.raises(lifecycle.IntelligentEvalNotFoundError): lifecycle.approve(seeded, "no-such-eval")