fischer-agentkit/tests/unit/experts/test_plan.py

289 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""TeamPlan / SubTask 数据模型单元测试 (hub-and-spoke 模式)"""
from __future__ import annotations
from agentkit.experts.plan import (
MergeStrategy,
PlanStatus,
SubTask,
SubTaskStatus,
TeamPlan,
)
# ── 辅助函数 ──────────────────────────────────────────────
def _make_subtask(
id: str = "subtask_1",
description: str = "分析数据",
assigned_expert: str = "analyst",
status: SubTaskStatus = SubTaskStatus.PENDING,
result: dict | None = None,
) -> SubTask:
"""创建测试用 SubTask 实例"""
return SubTask(
id=id,
description=description,
assigned_expert=assigned_expert,
status=status,
result=result,
)
def _make_valid_plan() -> TeamPlan:
"""创建一个有效的 hub-and-spoke 执行计划"""
subtasks = [
_make_subtask(id="s1", description="分析需求", assigned_expert="analyst"),
_make_subtask(id="s2", description="设计架构", assigned_expert="architect"),
_make_subtask(id="s3", description="编写代码", assigned_expert="coder"),
]
return TeamPlan(
id="plan_001",
task="实现用户登录功能",
subtasks=subtasks,
status=PlanStatus.DRAFT,
lead_expert="architect",
)
# ── MergeStrategy 测试 ────────────────────────────────────
class TestMergeStrategy:
"""MergeStrategy 枚举测试"""
def test_only_best_strategy_exists(self):
"""hub-and-spoke 模式仅保留 BEST 策略"""
assert MergeStrategy.BEST == "best"
# 确保只有 BEST 一个值
assert len(list(MergeStrategy)) == 1
def test_no_vote_or_fusion(self):
"""VOTE 和 FUSION 已被移除"""
assert not hasattr(MergeStrategy, "VOTE")
assert not hasattr(MergeStrategy, "FUSION")
# ── PlanStatus 测试 ───────────────────────────────────────
class TestPlanStatus:
"""PlanStatus 枚举测试"""
def test_statuses_exist(self):
"""必要的计划状态都存在"""
assert PlanStatus.DRAFT == "draft"
assert PlanStatus.EXECUTING == "executing"
assert PlanStatus.COMPLETED == "completed"
assert PlanStatus.FAILED == "failed"
assert PlanStatus.FALLBACK == "fallback"
def test_no_confirmed_status(self):
"""CONFIRMED 状态已被移除hub-and-spoke 无需确认阶段)"""
assert not hasattr(PlanStatus, "CONFIRMED")
# ── SubTaskStatus 测试 ────────────────────────────────────
class TestSubTaskStatus:
"""SubTaskStatus 枚举测试"""
def test_statuses_exist(self):
"""子任务状态都存在"""
assert SubTaskStatus.PENDING == "pending"
assert SubTaskStatus.RUNNING == "running"
assert SubTaskStatus.COMPLETED == "completed"
assert SubTaskStatus.FAILED == "failed"
# ── SubTask 测试 ──────────────────────────────────────────
class TestSubTask:
"""SubTask 数据模型测试"""
def test_creation_with_all_fields(self):
"""创建 SubTask 并设置所有字段"""
subtask = SubTask(
id="subtask_a",
description="竞品分析",
assigned_expert="analyst",
status=SubTaskStatus.RUNNING,
result={"report": "竞品分析报告"},
)
assert subtask.id == "subtask_a"
assert subtask.description == "竞品分析"
assert subtask.assigned_expert == "analyst"
assert subtask.status == SubTaskStatus.RUNNING
assert subtask.result == {"report": "竞品分析报告"}
def test_default_values(self):
"""默认值:自动生成 idPENDING 状态"""
subtask = SubTask(description="测试任务")
assert subtask.id is not None
assert len(subtask.id) > 0
assert subtask.description == "测试任务"
assert subtask.assigned_expert == ""
assert subtask.status == SubTaskStatus.PENDING
assert subtask.result is None
def test_to_dict_from_dict_roundtrip(self):
"""to_dict / from_dict 往返序列化"""
subtask = SubTask(
id="roundtrip_subtask",
description="往返测试",
assigned_expert="tester",
status=SubTaskStatus.COMPLETED,
result={"key": "value"},
)
d = subtask.to_dict()
restored = SubTask.from_dict(d)
assert restored.id == subtask.id
assert restored.description == subtask.description
assert restored.assigned_expert == subtask.assigned_expert
assert restored.status == subtask.status
assert restored.result == subtask.result
def test_to_dict_structure(self):
"""to_dict 返回正确的字典结构"""
subtask = _make_subtask(
id="struct_test",
description="结构测试",
assigned_expert="dev",
status=SubTaskStatus.RUNNING,
result={"output": "data"},
)
d = subtask.to_dict()
assert d["id"] == "struct_test"
assert d["description"] == "结构测试"
assert d["assigned_expert"] == "dev"
assert d["status"] == "running"
assert d["result"] == {"output": "data"}
# ── TeamPlan 测试 ─────────────────────────────────────────
class TestTeamPlan:
"""TeamPlan 数据模型测试"""
def test_creation(self):
"""创建 TeamPlan"""
plan = _make_valid_plan()
assert plan.id == "plan_001"
assert plan.task == "实现用户登录功能"
assert len(plan.subtasks) == 3
assert plan.status == PlanStatus.DRAFT
assert plan.lead_expert == "architect"
def test_default_values(self):
"""默认值:自动生成 id空子任务列表"""
plan = TeamPlan(task="测试任务")
assert plan.id is not None
assert plan.task == "测试任务"
assert plan.subtasks == []
assert plan.status == PlanStatus.DRAFT
assert plan.lead_expert == ""
def test_to_dict_from_dict_roundtrip(self):
"""to_dict / from_dict 往返序列化"""
plan = _make_valid_plan()
d = plan.to_dict()
restored = TeamPlan.from_dict(d)
assert restored.id == plan.id
assert restored.task == plan.task
assert len(restored.subtasks) == len(plan.subtasks)
assert restored.status == plan.status
assert restored.lead_expert == plan.lead_expert
for original, restored_st in zip(plan.subtasks, restored.subtasks):
assert restored_st.id == original.id
assert restored_st.description == original.description
assert restored_st.assigned_expert == original.assigned_expert
assert restored_st.status == original.status
def test_get_subtask_by_id(self):
"""get_subtask 根据 ID 获取子任务"""
plan = _make_valid_plan()
st = plan.get_subtask("s2")
assert st is not None
assert st.id == "s2"
assert st.description == "设计架构"
def test_get_subtask_with_nonexistent_id_returns_none(self):
"""get_subtask 对不存在的 ID 返回 None"""
plan = _make_valid_plan()
assert plan.get_subtask("nonexistent") is None
def test_update_subtask_status(self):
"""update_subtask_status 更新子任务状态和结果"""
plan = _make_valid_plan()
plan.update_subtask_status("s1", SubTaskStatus.COMPLETED, {"output": "分析完成"})
st = plan.get_subtask("s1")
assert st is not None
assert st.status == SubTaskStatus.COMPLETED
assert st.result == {"output": "分析完成"}
def test_update_subtask_status_without_result(self):
"""update_subtask_status 不传 result 时不覆盖已有 result"""
plan = _make_valid_plan()
plan.update_subtask_status("s1", SubTaskStatus.COMPLETED, {"output": "done"})
plan.update_subtask_status("s1", SubTaskStatus.RUNNING)
st = plan.get_subtask("s1")
assert st is not None
assert st.status == SubTaskStatus.RUNNING
assert st.result == {"output": "done"}
def test_completed_subtasks_property(self):
"""completed_subtasks 返回已完成的子任务"""
plan = _make_valid_plan()
plan.update_subtask_status("s1", SubTaskStatus.COMPLETED)
plan.update_subtask_status("s2", SubTaskStatus.COMPLETED)
plan.update_subtask_status("s3", SubTaskStatus.FAILED)
completed = plan.completed_subtasks
assert len(completed) == 2
assert {st.id for st in completed} == {"s1", "s2"}
def test_failed_subtasks_property(self):
"""failed_subtasks 返回失败的子任务"""
plan = _make_valid_plan()
plan.update_subtask_status("s1", SubTaskStatus.COMPLETED)
plan.update_subtask_status("s2", SubTaskStatus.FAILED)
plan.update_subtask_status("s3", SubTaskStatus.FAILED)
failed = plan.failed_subtasks
assert len(failed) == 2
assert {st.id for st in failed} == {"s2", "s3"}
def test_all_done_property_when_all_completed(self):
"""all_done 当所有子任务完成时返回 True"""
plan = _make_valid_plan()
for st in plan.subtasks:
plan.update_subtask_status(st.id, SubTaskStatus.COMPLETED)
assert plan.all_done is True
def test_all_done_property_when_some_failed(self):
"""all_done 当所有子任务完成或失败时返回 True"""
plan = _make_valid_plan()
plan.update_subtask_status("s1", SubTaskStatus.COMPLETED)
plan.update_subtask_status("s2", SubTaskStatus.FAILED)
plan.update_subtask_status("s3", SubTaskStatus.COMPLETED)
assert plan.all_done is True
def test_all_done_property_when_pending(self):
"""all_done 当有子任务未完成时返回 False"""
plan = _make_valid_plan()
plan.update_subtask_status("s1", SubTaskStatus.COMPLETED)
# s2 and s3 still pending
assert plan.all_done is False
def test_all_done_property_empty_plan(self):
"""all_done 当没有子任务时返回 Truevacuous truth"""
plan = TeamPlan(task="空计划")
assert plan.all_done is True