669 lines
22 KiB
Python
669 lines
22 KiB
Python
"""TeamOrchestrator 单元测试"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from agentkit.core.handoff_transport import InProcessHandoffTransport
|
|
from agentkit.experts.config import ExpertConfig
|
|
from agentkit.experts.expert import Expert
|
|
from agentkit.experts.orchestrator import TeamOrchestrator
|
|
from agentkit.experts.plan import (
|
|
CollaborationPlan,
|
|
MergeStrategy,
|
|
ParallelType,
|
|
PhaseStatus,
|
|
PlanPhase,
|
|
PlanStatus,
|
|
)
|
|
from agentkit.experts.team import ExpertTeam, TeamStatus
|
|
|
|
|
|
# ── 辅助函数 ──────────────────────────────────────────────
|
|
|
|
|
|
def _make_expert_config(
|
|
name: str = "test_expert",
|
|
is_lead: bool = False,
|
|
) -> ExpertConfig:
|
|
"""创建测试用 ExpertConfig"""
|
|
return ExpertConfig(
|
|
name=name,
|
|
agent_type="expert",
|
|
persona="测试专家",
|
|
thinking_style="逻辑推理",
|
|
bound_skills=["skill_a"],
|
|
is_lead=is_lead,
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "测试"},
|
|
)
|
|
|
|
|
|
def _make_mock_expert(
|
|
name: str = "test_expert",
|
|
is_lead: bool = False,
|
|
is_active: bool = True,
|
|
) -> MagicMock:
|
|
"""创建 mock Expert"""
|
|
config = _make_expert_config(name=name, is_lead=is_lead)
|
|
expert = MagicMock(spec=Expert)
|
|
expert.config = config
|
|
expert.is_active = is_active
|
|
expert.team_id = None
|
|
expert.get_capabilities_summary.return_value = {
|
|
"name": name,
|
|
"persona": config.persona,
|
|
"thinking_style": config.thinking_style,
|
|
"bound_skills": config.bound_skills,
|
|
"is_lead": is_lead,
|
|
}
|
|
return expert
|
|
|
|
|
|
def _make_team_with_experts(
|
|
expert_names: list[str] | None = None,
|
|
lead_name: str = "lead",
|
|
) -> ExpertTeam:
|
|
"""创建包含 mock experts 的 ExpertTeam"""
|
|
team = ExpertTeam()
|
|
transport = AsyncMock(spec=InProcessHandoffTransport)
|
|
team._handoff_transport = transport
|
|
|
|
if expert_names is None:
|
|
expert_names = [lead_name, "member1", "member2"]
|
|
|
|
for name in expert_names:
|
|
is_lead = name == lead_name
|
|
expert = _make_mock_expert(name=name, is_lead=is_lead)
|
|
team._experts[name] = expert
|
|
if is_lead:
|
|
team._lead_expert_name = name
|
|
|
|
return team
|
|
|
|
|
|
def _make_serial_plan(
|
|
plan_id: str = "plan_1",
|
|
task: str = "测试任务",
|
|
lead_expert: str = "lead",
|
|
num_phases: int = 1,
|
|
) -> CollaborationPlan:
|
|
"""创建串行阶段的 CollaborationPlan"""
|
|
phases = []
|
|
for i in range(num_phases):
|
|
deps = [f"phase_{i}"] if i > 0 else []
|
|
phases.append(
|
|
PlanPhase(
|
|
id=f"phase_{i + 1}",
|
|
name=f"阶段{i + 1}",
|
|
assigned_expert=lead_expert,
|
|
task_description=f"执行任务{i + 1}",
|
|
depends_on=deps,
|
|
parallel_type=ParallelType.SERIAL,
|
|
)
|
|
)
|
|
return CollaborationPlan(
|
|
id=plan_id,
|
|
task=task,
|
|
phases=phases,
|
|
lead_expert=lead_expert,
|
|
)
|
|
|
|
|
|
def _make_parallel_plan(
|
|
plan_id: str = "plan_parallel",
|
|
task: str = "并行测试任务",
|
|
parallel_type: ParallelType = ParallelType.SUBTASK_PARALLEL,
|
|
merge_strategy: MergeStrategy | None = None,
|
|
) -> CollaborationPlan:
|
|
"""创建并行阶段的 CollaborationPlan"""
|
|
phases = [
|
|
PlanPhase(
|
|
id="phase_1",
|
|
name="并行阶段1",
|
|
assigned_expert="member1",
|
|
task_description="并行任务1",
|
|
parallel_type=parallel_type,
|
|
merge_strategy=merge_strategy,
|
|
),
|
|
PlanPhase(
|
|
id="phase_2",
|
|
name="并行阶段2",
|
|
assigned_expert="member2",
|
|
task_description="并行任务2",
|
|
parallel_type=parallel_type,
|
|
merge_strategy=merge_strategy,
|
|
),
|
|
]
|
|
return CollaborationPlan(
|
|
id=plan_id,
|
|
task=task,
|
|
phases=phases,
|
|
lead_expert="lead",
|
|
)
|
|
|
|
|
|
# ── 串行阶段执行测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestSerialPhaseExecution:
|
|
"""串行阶段执行测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_single_serial_phase_completes(self):
|
|
"""单个串行阶段执行完成"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=1)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
assert "phase_1" in result["phase_results"]
|
|
assert plan.phases[0].status == PhaseStatus.COMPLETED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_serial_phases_in_order(self):
|
|
"""多个串行阶段按依赖顺序执行"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=3)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
assert len(result["phase_results"]) == 3
|
|
# All phases should be completed
|
|
for phase in plan.phases:
|
|
assert phase.status == PhaseStatus.COMPLETED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_serial_phase_sets_plan_and_team_status(self):
|
|
"""执行计划时设置 plan 和 team 状态"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan()
|
|
|
|
await orchestrator.execute_plan(plan)
|
|
|
|
assert plan.status == PlanStatus.COMPLETED
|
|
assert team._status == TeamStatus.COMPLETED
|
|
|
|
|
|
# ── 子任务并行阶段执行测试 ────────────────────────────────
|
|
|
|
|
|
class TestSubtaskParallelExecution:
|
|
"""子任务并行阶段执行测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subtask_parallel_phases_execute(self):
|
|
"""子任务并行阶段并行执行"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_parallel_plan(parallel_type=ParallelType.SUBTASK_PARALLEL)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
assert "phase_1" in result["phase_results"]
|
|
assert "phase_2" in result["phase_results"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subtask_parallel_phase_failure_recorded(self):
|
|
"""子任务并行阶段失败时记录错误"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_parallel_plan(parallel_type=ParallelType.SUBTASK_PARALLEL)
|
|
|
|
# Mock _execute_phase to raise for one phase
|
|
original_execute = orchestrator._execute_phase
|
|
call_count = 0
|
|
|
|
async def mock_execute_phase(phase, p, pr):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if phase.id == "phase_1":
|
|
raise RuntimeError("Simulated failure")
|
|
return await original_execute_phase(phase, p, pr)
|
|
|
|
with patch.object(
|
|
orchestrator, "_execute_phase", side_effect=mock_execute_phase
|
|
):
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
# The exception should be caught by asyncio.gather(return_exceptions=True)
|
|
assert "phase_1" in result["phase_results"]
|
|
assert "error" in result["phase_results"]["phase_1"]
|
|
|
|
|
|
# ── 竞争并行阶段测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestCompetitiveParallelExecution:
|
|
"""竞争并行阶段执行测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_competitive_parallel_best_strategy(self):
|
|
"""竞争并行阶段使用 BEST 合并策略"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_parallel_plan(
|
|
parallel_type=ParallelType.COMPETITIVE_PARALLEL,
|
|
merge_strategy=MergeStrategy.BEST,
|
|
)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
# Competitive phases are merged into one result per phase
|
|
for phase_id in ["phase_1", "phase_2"]:
|
|
assert phase_id in result["phase_results"]
|
|
phase_result = result["phase_results"][phase_id]
|
|
assert phase_result.get("merged") is True
|
|
assert phase_result.get("strategy") == "best"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_competitive_parallel_vote_strategy(self):
|
|
"""竞争并行阶段使用 VOTE 合并策略"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_parallel_plan(
|
|
parallel_type=ParallelType.COMPETITIVE_PARALLEL,
|
|
merge_strategy=MergeStrategy.VOTE,
|
|
)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
for phase_id in ["phase_1", "phase_2"]:
|
|
phase_result = result["phase_results"][phase_id]
|
|
assert phase_result.get("merged") is True
|
|
assert phase_result.get("strategy") == "vote"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_competitive_parallel_fusion_strategy(self):
|
|
"""竞争并行阶段使用 FUSION 合并策略"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_parallel_plan(
|
|
parallel_type=ParallelType.COMPETITIVE_PARALLEL,
|
|
merge_strategy=MergeStrategy.FUSION,
|
|
)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
for phase_id in ["phase_1", "phase_2"]:
|
|
phase_result = result["phase_results"][phase_id]
|
|
assert phase_result.get("merged") is True
|
|
assert phase_result.get("strategy") == "fusion"
|
|
assert phase_result.get("fused_from") == 3 # 3 active experts
|
|
|
|
|
|
# ── 里程碑检查点测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestMilestoneCheckpoint:
|
|
"""里程碑检查点测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_milestone_pass(self):
|
|
"""里程碑检查通过"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = CollaborationPlan(
|
|
id="plan_milestone",
|
|
task="里程碑测试",
|
|
phases=[
|
|
PlanPhase(
|
|
id="phase_1",
|
|
name="带里程碑阶段",
|
|
assigned_expert="lead",
|
|
task_description="执行带里程碑的任务",
|
|
milestone="输出质量达标",
|
|
)
|
|
],
|
|
lead_expert="lead",
|
|
)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
assert plan.phases[0].status == PhaseStatus.COMPLETED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_milestone_fail_phase_failed(self):
|
|
"""里程碑检查失败 → 阶段状态为 FAILED"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = CollaborationPlan(
|
|
id="plan_milestone_fail",
|
|
task="里程碑失败测试",
|
|
phases=[
|
|
PlanPhase(
|
|
id="phase_1",
|
|
name="带里程碑阶段",
|
|
assigned_expert="lead",
|
|
task_description="执行带里程碑的任务",
|
|
milestone="输出质量达标",
|
|
)
|
|
],
|
|
lead_expert="lead",
|
|
)
|
|
|
|
# Mock _check_milestone to return False
|
|
with patch.object(
|
|
orchestrator, "_check_milestone", return_value=False
|
|
):
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert plan.phases[0].status == PhaseStatus.FAILED
|
|
# Phase failed → retry → still failed → fallback
|
|
assert result["status"] == "fallback"
|
|
|
|
|
|
# ── 重试与回退测试 ────────────────────────────────────────
|
|
|
|
|
|
class TestRetryAndFallback:
|
|
"""重试与回退测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_phase_failure_triggers_retry(self):
|
|
"""阶段失败触发重试"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=1)
|
|
|
|
# Mock _execute_phase: first call returns None, second call succeeds
|
|
call_count = 0
|
|
|
|
async def mock_execute_phase(phase, p, pr):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
# First call fails
|
|
p.update_phase_status(phase.id, PhaseStatus.FAILED)
|
|
return None
|
|
# Retry succeeds — simulate a successful phase execution
|
|
p.update_phase_status(phase.id, PhaseStatus.COMPLETED, {"output": "retry ok"})
|
|
return {"output": "retry ok"}
|
|
|
|
with patch.object(
|
|
orchestrator, "_execute_phase", side_effect=mock_execute_phase
|
|
):
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
# After retry, the phase should succeed
|
|
assert call_count == 2
|
|
assert result["status"] == "completed"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_failure_triggers_fallback(self):
|
|
"""重试仍然失败 → 回退到单 Agent 模式"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=1)
|
|
|
|
# Mock _execute_phase to always return None (failure)
|
|
async def mock_execute_phase(phase, p, pr):
|
|
plan.update_phase_status(phase.id, PhaseStatus.FAILED)
|
|
return None
|
|
|
|
with patch.object(
|
|
orchestrator, "_execute_phase", side_effect=mock_execute_phase
|
|
):
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "fallback"
|
|
assert plan.status == PlanStatus.FALLBACK
|
|
|
|
|
|
# ── 最大交互轮次测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestMaxInteractionRounds:
|
|
"""最大交互轮次限制测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_interaction_rounds_limit(self):
|
|
"""超过最大交互轮次时停止执行"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
orchestrator.MAX_INTERACTION_ROUNDS = 1
|
|
|
|
# Create a plan with many phases that would take many rounds
|
|
plan = _make_serial_plan(num_phases=5)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
# Should stop after 1 round, not completing all phases
|
|
# Only the first phase should complete (1 interaction round)
|
|
assert orchestrator._interaction_count >= 1
|
|
|
|
|
|
# ── 无效计划测试 ──────────────────────────────────────────
|
|
|
|
|
|
class TestInvalidPlan:
|
|
"""无效计划测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_plan_returns_failed_status(self):
|
|
"""无效计划返回 failed 状态"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
|
|
# Create invalid plan with circular dependency
|
|
plan = CollaborationPlan(
|
|
id="invalid_plan",
|
|
task="无效任务",
|
|
phases=[
|
|
PlanPhase(
|
|
id="p1",
|
|
name="阶段1",
|
|
assigned_expert="lead",
|
|
task_description="t1",
|
|
depends_on=["p2"],
|
|
),
|
|
PlanPhase(
|
|
id="p2",
|
|
name="阶段2",
|
|
assigned_expert="lead",
|
|
task_description="t2",
|
|
depends_on=["p1"],
|
|
),
|
|
],
|
|
lead_expert="lead",
|
|
)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "failed"
|
|
assert "errors" in result
|
|
assert len(result["errors"]) > 0
|
|
|
|
|
|
# ── 结果综合测试 ──────────────────────────────────────────
|
|
|
|
|
|
class TestSynthesizeResults:
|
|
"""结果综合测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_synthesize_results(self):
|
|
"""综合所有阶段结果"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=2)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
assert result["status"] == "completed"
|
|
final = result["result"]
|
|
assert final["task"] == "测试任务"
|
|
assert final["phases_completed"] == 2
|
|
assert final["phases_total"] == 2
|
|
assert len(final["results"]) == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_synthesize_results_only_completed_phases(self):
|
|
"""只综合已完成阶段的结果"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
|
|
plan = CollaborationPlan(
|
|
id="plan_partial",
|
|
task="部分完成测试",
|
|
phases=[
|
|
PlanPhase(
|
|
id="phase_1",
|
|
name="完成阶段",
|
|
assigned_expert="lead",
|
|
task_description="任务1",
|
|
),
|
|
PlanPhase(
|
|
id="phase_2",
|
|
name="依赖阶段",
|
|
assigned_expert="member1",
|
|
task_description="任务2",
|
|
depends_on=["phase_1"],
|
|
),
|
|
],
|
|
lead_expert="lead",
|
|
)
|
|
|
|
# Manually set phase_1 as completed, phase_2 as pending
|
|
plan.update_phase_status("phase_1", PhaseStatus.COMPLETED, {"output": "done"})
|
|
|
|
# Synthesize directly
|
|
phase_results = {"phase_1": {"output": "done"}}
|
|
result = await orchestrator._synthesize_results(plan, phase_results)
|
|
|
|
assert result["phases_completed"] == 1
|
|
assert result["phases_total"] == 2
|
|
|
|
|
|
# ── 事件广播测试 ──────────────────────────────────────────
|
|
|
|
|
|
class TestBroadcastEvent:
|
|
"""事件广播测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_event_sends_to_transport(self):
|
|
"""广播事件通过 handoff_transport 发送"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
|
|
await orchestrator._broadcast_event("test_event", {"key": "value"})
|
|
|
|
team._handoff_transport.send.assert_awaited_once()
|
|
call_args = team._handoff_transport.send.call_args
|
|
assert call_args[0][0] == team._team_channel
|
|
message = call_args[0][1]
|
|
assert message["type"] == "test_event"
|
|
assert message["key"] == "value"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_event_no_transport(self):
|
|
"""没有 handoff_transport 时不报错"""
|
|
team = _make_team_with_experts()
|
|
team._handoff_transport = None
|
|
orchestrator = TeamOrchestrator(team)
|
|
|
|
# Should not raise
|
|
await orchestrator._broadcast_event("test_event", {"key": "value"})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_phase_execution_broadcasts_events(self):
|
|
"""阶段执行时广播 phase_started 和 phase_completed 事件"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=1)
|
|
|
|
await orchestrator.execute_plan(plan)
|
|
|
|
calls = team._handoff_transport.send.call_args_list
|
|
event_types = [c[0][1]["type"] for c in calls]
|
|
assert "phase_started" in event_types
|
|
assert "phase_completed" in event_types
|
|
|
|
|
|
# ── 竞争并行全部失败测试 ──────────────────────────────────
|
|
|
|
|
|
class TestCompetitiveAllFail:
|
|
"""竞争并行全部失败测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_competitors_fail(self):
|
|
"""所有竞争者都失败时触发 fallback"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
|
|
# Mock _run_competitor to always raise
|
|
async def mock_run_competitor(expert, phase):
|
|
raise RuntimeError("Competitor failed")
|
|
|
|
with patch.object(
|
|
orchestrator, "_run_competitor", side_effect=mock_run_competitor
|
|
):
|
|
plan = _make_parallel_plan(
|
|
parallel_type=ParallelType.COMPETITIVE_PARALLEL,
|
|
merge_strategy=MergeStrategy.BEST,
|
|
)
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
# All competitors failed → triggers fallback
|
|
assert result["status"] == "fallback"
|
|
|
|
|
|
# ── Expert 不可用测试 ────────────────────────────────────
|
|
|
|
|
|
class TestExpertUnavailable:
|
|
"""Expert 不可用测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inactive_expert_causes_phase_failure(self):
|
|
"""分配的 Expert 不活跃导致阶段失败"""
|
|
team = _make_team_with_experts()
|
|
# Mark the lead expert as inactive
|
|
team._experts["lead"].is_active = False
|
|
orchestrator = TeamOrchestrator(team)
|
|
plan = _make_serial_plan(num_phases=1)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
# Phase should fail because expert is not active → retry → still fail → fallback
|
|
assert result["status"] == "fallback"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nonexistent_expert_causes_phase_failure(self):
|
|
"""分配的 Expert 不存在导致阶段失败"""
|
|
team = _make_team_with_experts()
|
|
orchestrator = TeamOrchestrator(team)
|
|
|
|
plan = CollaborationPlan(
|
|
id="plan_no_expert",
|
|
task="无专家测试",
|
|
phases=[
|
|
PlanPhase(
|
|
id="phase_1",
|
|
name="无专家阶段",
|
|
assigned_expert="nonexistent_expert",
|
|
task_description="执行任务",
|
|
)
|
|
],
|
|
lead_expert="lead",
|
|
)
|
|
|
|
result = await orchestrator.execute_plan(plan)
|
|
|
|
# Expert doesn't exist → phase fails → retry → still fails → fallback
|
|
assert result["status"] == "fallback"
|