架构重构(候选 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 全绿
176 lines
6.0 KiB
Python
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")
|