168 lines
6.5 KiB
Python
168 lines
6.5 KiB
Python
"""Lifecycle contract tests for intelligent evaluations."""
|
|
|
|
import pytest
|
|
from agenteval.channels.base import ExchangeOutcome, SendResult
|
|
from agenteval.intelligent_eval import lifecycle
|
|
from agenteval.intelligent_eval.lifecycle import (
|
|
IntelligentEvalChannelError,
|
|
IntelligentEvalNotFoundError,
|
|
IntelligentEvalTransitionError,
|
|
)
|
|
from agenteval.intelligent_eval.models import IntelligentEvalSessionStatus, IntelligentEvalStatus
|
|
from agenteval.models import ChannelType, EvalTarget, PlatformType, TargetStatus
|
|
from agenteval.storage.repository import TargetRepository
|
|
|
|
|
|
@pytest.fixture()
|
|
def eval_session(db_session):
|
|
TargetRepository(db_session).create(
|
|
EvalTarget(
|
|
id="target-1",
|
|
name="测试对象",
|
|
platform=PlatformType.AI_DIGITAL_EMPLOYEE,
|
|
channel_type=ChannelType.TUTU_API,
|
|
channel_config={"base_url": "http://mock", "token": "token"},
|
|
status=TargetStatus.ACTIVE,
|
|
)
|
|
)
|
|
return db_session
|
|
|
|
|
|
def _create(session, *, name: str = "智能评估"):
|
|
return lifecycle.create_eval(
|
|
session,
|
|
name=name,
|
|
target_id="target-1",
|
|
goal="验证退货流程",
|
|
seeds={"personas": ["老客户"]},
|
|
intent="流程覆盖",
|
|
role_description="模拟用户",
|
|
)
|
|
|
|
|
|
def _start(session, *, name: str = "智能评估"):
|
|
evaluation = _create(session, name=name)
|
|
lifecycle.submit_plan(session, evaluation.id, {"dimensions": ["退货"]})
|
|
return lifecycle.approve(session, evaluation.id)
|
|
|
|
|
|
def test_lifecycle_owns_complete_evaluation_state_machine(eval_session) -> None:
|
|
evaluation = _create(eval_session)
|
|
assert evaluation.status is IntelligentEvalStatus.PLANNING
|
|
|
|
pending = lifecycle.submit_plan(eval_session, evaluation.id, {"dimensions": ["退货"]})
|
|
assert pending.status is IntelligentEvalStatus.PENDING_APPROVAL
|
|
assert pending.plan == {"dimensions": ["退货"]}
|
|
|
|
executing = lifecycle.approve(eval_session, evaluation.id)
|
|
assert executing.status is IntelligentEvalStatus.EXECUTING
|
|
assert executing.started_at is not None
|
|
|
|
completed = lifecycle.submit_report(eval_session, evaluation.id, {"summary": "done"})
|
|
assert completed.status is IntelligentEvalStatus.COMPLETED
|
|
assert completed.report == {"summary": "done"}
|
|
assert completed.completed_at is not None
|
|
|
|
|
|
def test_lifecycle_rejects_invalid_or_repeated_transitions(eval_session) -> None:
|
|
evaluation = _create(eval_session)
|
|
|
|
with pytest.raises(IntelligentEvalTransitionError):
|
|
lifecycle.approve(eval_session, evaluation.id)
|
|
|
|
lifecycle.submit_plan(eval_session, evaluation.id, {"dimensions": ["退货"]})
|
|
with pytest.raises(IntelligentEvalTransitionError):
|
|
lifecycle.submit_plan(eval_session, evaluation.id, {"dimensions": ["重复"]})
|
|
|
|
|
|
def test_plan_transaction_failure_leaves_public_snapshot_unchanged(eval_session, monkeypatch) -> None:
|
|
evaluation = _create(eval_session)
|
|
monkeypatch.setattr(eval_session, "commit", lambda: (_ for _ in ()).throw(RuntimeError("commit failed")))
|
|
|
|
with pytest.raises(RuntimeError, match="commit failed"):
|
|
lifecycle.submit_plan(eval_session, evaluation.id, {"dimensions": ["退货"]})
|
|
|
|
monkeypatch.undo()
|
|
unchanged = lifecycle.get_eval(eval_session, evaluation.id)
|
|
assert unchanged.status is IntelligentEvalStatus.PLANNING
|
|
assert unchanged.plan is None
|
|
|
|
|
|
def test_session_ownership_and_close_invariants_are_lifecycle_rules(eval_session) -> None:
|
|
first = _start(eval_session, name="first")
|
|
second = _start(eval_session, name="second")
|
|
session_obj = lifecycle.open_session(
|
|
eval_session,
|
|
eval_id=first.id,
|
|
persona={"name": "老客户"},
|
|
goal="完成退货",
|
|
dimension="退货",
|
|
)
|
|
|
|
with pytest.raises(IntelligentEvalNotFoundError):
|
|
lifecycle.close_session(
|
|
eval_session,
|
|
eval_id=second.id,
|
|
session_id=session_obj.id,
|
|
verdict={"goal_achieved": False},
|
|
)
|
|
|
|
closed = lifecycle.close_session(
|
|
eval_session,
|
|
eval_id=first.id,
|
|
session_id=session_obj.id,
|
|
verdict={"goal_achieved": True},
|
|
)
|
|
assert closed.status is IntelligentEvalSessionStatus.COMPLETED
|
|
assert closed.verdict == {"goal_achieved": True}
|
|
|
|
|
|
class _ReplyChannel:
|
|
async def exchange(self, _content, *, on_sent, **_kwargs):
|
|
await on_sent(SendResult(ok=True, question_msg_id="message-1"))
|
|
return ExchangeOutcome.succeeded(correlation_id="message-1", reply="答复", latency_ms=12)
|
|
|
|
|
|
class _TimeoutChannel:
|
|
async def exchange(self, _content, *, on_sent, **_kwargs):
|
|
await on_sent(SendResult(ok=True, question_msg_id="message-1"))
|
|
return ExchangeOutcome.reply_timeout(correlation_id="message-1", latency_ms=30_000)
|
|
|
|
|
|
async def test_turn_ledger_is_observable_only_through_lifecycle(eval_session, monkeypatch) -> None:
|
|
evaluation = _start(eval_session)
|
|
session_obj = lifecycle.open_session(eval_session, eval_id=evaluation.id, persona={}, goal="完成退货")
|
|
monkeypatch.setattr(lifecycle.ChannelFactory, "create", lambda _target: _ReplyChannel())
|
|
|
|
result = await lifecycle.conduct_turn(
|
|
eval_session,
|
|
eval_id=evaluation.id,
|
|
session_id=session_obj.id,
|
|
content="你好",
|
|
)
|
|
|
|
messages = lifecycle.list_messages(eval_session, eval_id=evaluation.id, session_id=session_obj.id)
|
|
sessions = lifecycle.list_sessions(eval_session, evaluation.id)
|
|
assert result == {"reply": "答复", "latency_ms": 12, "turn_count": 1}
|
|
assert [(item.role, item.content) for item in messages] == [("user", "你好"), ("assistant", "答复")]
|
|
assert sessions[0].turn_count == 1
|
|
|
|
|
|
async def test_timeout_preserves_sent_message_and_turn_count(eval_session, monkeypatch) -> None:
|
|
evaluation = _start(eval_session)
|
|
session_obj = lifecycle.open_session(eval_session, eval_id=evaluation.id, persona={}, goal="完成退货")
|
|
monkeypatch.setattr(lifecycle.ChannelFactory, "create", lambda _target: _TimeoutChannel())
|
|
|
|
with pytest.raises(IntelligentEvalChannelError, match="超时"):
|
|
await lifecycle.conduct_turn(
|
|
eval_session,
|
|
eval_id=evaluation.id,
|
|
session_id=session_obj.id,
|
|
content="你好",
|
|
)
|
|
|
|
messages = lifecycle.list_messages(eval_session, eval_id=evaluation.id, session_id=session_obj.id)
|
|
sessions = lifecycle.list_sessions(eval_session, evaluation.id)
|
|
assert [(item.role, item.content) for item in messages] == [("user", "你好")]
|
|
assert sessions[0].turn_count == 1
|