AgentEvalTool/tests/unit/test_intelligent_eval_lifecycle_transitions.py
sinohqb 3705945a7d test: 完整测试覆盖补全(+163 用例)
架构重构(候选 1-6):
- storage/repository.py 按域拆分为包(target/scenario/run/campaign/result)
- storage/db.py 按域拆分为包(eval/campaign/file/model_config/intelligent_eval)
- intelligent_eval/lifecycle.py 按状态机阶段拆分为包
- services/runs.py 编排逻辑下沉
- Campaigns.tsx 拆分为 campaigns/ 子组件

测试补全(候选 7):
前端(+125 用例,107→232):
- utils/ 纯函数:date/campaignTime/ruleLabels/fileTree/fileFormat/colors
- stores/tabStore 状态管理
- 核心组件:FormDrawer/PageWrapper/ChatBubble/GeneratedMessages/SectionHeader/StatCard/TurnList
- 业务组件:CaseBlock/CaseDetail/RuleOverview/WindowTimeline/RunList/TabBar/CampaignRunTimeline
- 文件管理:FileCategoryTree/FileTable
- hooks:sessionReducer/useFiles/useRunSession

后端(+38 用例,916→954):
- targets API CRUD + 404 路径
- WebSocket 连接管理器
- proxy 头部重写(CSP/X-Frame-Options)
- target 仓储 update 方法
- app 健康检查 + SPA 404
- scenarios 模板端点 + 404
- files API 边缘分支(404 场景 + 500 兜底)
- files service update_category
- 智能评估状态机迁移测试

门禁状态:
- 前端:tsc 干净 + 232 passed
- 后端:954 passed + ruff 全绿
2026-08-24 15:56:09 +08:00

176 lines
6.0 KiB
Python

"""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")