"""TeamOrchestrator 单元测试 (流水线模式) 测试覆盖: - 流水线执行(阶段依赖、拓扑排序) - 并行阶段执行 - 上下文隔离(独立 Agent 实例) - SharedWorkspace 数据传递 - 阶段失败与依赖失败传播 - 全失败 fallback - 事件广播(phase_started/phase_completed/phase_failed) - 模型路由(_get_model) - LLM 分解失败容错 - 循环依赖检测 - 无效专家引用 fallback - TeamStatus.PLANNING 状态流转 """ from __future__ import annotations import json from unittest.mock import AsyncMock, MagicMock, patch import pytest from agentkit.core.handoff_transport import InProcessHandoffTransport from agentkit.core.protocol import TaskResult, TaskStatus from agentkit.experts.config import ExpertConfig from agentkit.experts.expert import Expert from agentkit.experts.orchestrator import TeamOrchestrator from agentkit.experts.plan import PhaseStatus, PlanPhase, PlanStatus from agentkit.experts.team import ExpertTeam, TeamStatus # ── 辅助函数 ────────────────────────────────────────────── def _make_expert_config( name: str = "test_expert", is_lead: bool = False, llm: dict | None = None, ) -> 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": "测试"}, llm=llm, ) def _make_mock_expert( name: str = "test_expert", is_lead: bool = False, is_active: bool = True, llm: dict | None = None, ) -> MagicMock: """创建 mock Expert""" config = _make_expert_config(name=name, is_lead=is_lead, llm=llm) 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, } # Mock agent.execute() to return a successful TaskResult mock_agent = MagicMock() mock_agent.execute = AsyncMock(return_value=TaskResult( task_id="test", agent_name=name, status=TaskStatus.COMPLETED.value, output_data={"content": f"Result from {name}"}, error_message=None, started_at=None, completed_at=None, )) # No LLM gateway by default (tests single-phase path) mock_agent._llm_gateway = None expert.agent = mock_agent return expert def _make_team_with_experts( expert_names: list[str] | None = None, lead_name: str = "lead", pool: MagicMock | None = None, ) -> ExpertTeam: """创建包含 mock experts 的 ExpertTeam""" team = ExpertTeam() transport = AsyncMock(spec=InProcessHandoffTransport) team._handoff_transport = transport if pool is not None: team._pool = pool 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_mock_llm_gateway( phases: list[dict] | None = None, synthesis_content: str = "综合结果", ) -> MagicMock: """创建 mock LLM gateway. If phases is provided, the gateway returns a JSON array of phases for decomposition. Otherwise returns a simple response for synthesis. """ gateway = AsyncMock() if phases: phases_json = json.dumps(phases) decomp_response = MagicMock() decomp_response.content = phases_json synth_response = MagicMock() synth_response.content = synthesis_content # U3: 分歧检测会在 decomposition 与 synthesis 之间插入额外的 LLM 调用, # 因此用函数式 side_effect:首次返回 decomposition,其余一律返回 synthesis。 call_count = [0] async def chat_side_effect(messages, model=None, **kwargs): call_count[0] += 1 if call_count[0] == 1: return decomp_response return synth_response gateway.chat = AsyncMock(side_effect=chat_side_effect) else: response = MagicMock() response.content = synthesis_content gateway.chat = AsyncMock(return_value=response) return gateway def _make_mock_pool() -> MagicMock: """创建 mock AgentPool,模拟上下文隔离的 agent 创建""" pool = MagicMock() pool.create_agent = AsyncMock(side_effect=lambda config: MagicMock( execute=AsyncMock(return_value=TaskResult( task_id="test", agent_name=config.name, status=TaskStatus.COMPLETED.value, output_data={"content": f"Isolated result from {config.name}"}, error_message=None, started_at=None, completed_at=None, )) )) pool.remove_agent = AsyncMock() return pool # ── 流水线执行测试 ──────────────────────────────────────── class TestPipelineExecution: """流水线模式执行测试""" @pytest.mark.asyncio async def test_execute_single_phase_completes(self): """无 LLM 时,任务作为单个阶段执行完成""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) result = await orchestrator.execute("测试任务") assert result["status"] == "completed" assert "result" in result assert "phase_results" in result assert "plan" in result assert team.status == TeamStatus.COMPLETED @pytest.mark.asyncio async def test_execute_sets_team_status_completed(self): """执行完成后设置 team 状态为 COMPLETED""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") assert team.status == TeamStatus.COMPLETED @pytest.mark.asyncio async def test_pipeline_sequential_execution(self): """3 阶段(A→B→C)按序执行""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) # LLM 分解为 3 个串行阶段 gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]}, {"name": "C", "assigned_expert": "member2", "task_description": "阶段C", "depends_on": ["B"]}, ] ) team._experts["lead"].agent._llm_gateway = gateway # Track execution order execution_order: list[str] = [] for name in ["lead", "member1", "member2"]: original_execute = team._experts[name].agent.execute async def tracking_execute(task_msg, _orig=original_execute, _name=name): execution_order.append(_name) return await _orig(task_msg) team._experts[name].agent.execute = tracking_execute result = await orchestrator.execute("串行任务") assert result["status"] == "completed" plan = result["plan"] assert len(plan.phases) == 3 # All phases should be completed for ph in plan.phases: assert ph.status == PhaseStatus.COMPLETED # Verify sequential execution order: A(lead) → B(member1) → C(member2) assert execution_order == ["lead", "member1", "member2"] @pytest.mark.asyncio async def test_pipeline_parallel_phases(self): """2 个无依赖阶段并行执行""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": []}, {"name": "C", "assigned_expert": "member2", "task_description": "阶段C", "depends_on": ["A", "B"]}, ] ) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("并行任务") assert result["status"] == "completed" plan = result["plan"] assert len(plan.phases) == 3 # A and B should be in the same layer (parallel) layers = plan.topological_sort() assert len(layers) == 2 # [A, B], [C] assert len(layers[0]) == 2 # A and B parallel @pytest.mark.asyncio async def test_pipeline_emits_team_formed_event(self): """执行时广播 team_formed 事件""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") calls = team._handoff_transport.send.call_args_list event_types = [c[0][1]["type"] for c in calls] assert "team_formed" in event_types @pytest.mark.asyncio async def test_pipeline_emits_plan_update_with_phases(self): """执行时广播 plan_update 事件(包含阶段列表)""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") calls = team._handoff_transport.send.call_args_list plan_updates = [c for c in calls if c[0][1].get("type") == "plan_update"] assert len(plan_updates) >= 1 assert "plan_phases" in plan_updates[0][0][1] @pytest.mark.asyncio async def test_pipeline_emits_team_synthesis_event(self): """执行完成时广播 team_synthesis 事件""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") calls = team._handoff_transport.send.call_args_list event_types = [c[0][1]["type"] for c in calls] assert "team_synthesis" in event_types # ── 阶段事件广播测试 ────────────────────────────────────── class TestPhaseEvents: """阶段事件广播测试""" @pytest.mark.asyncio async def test_emits_phase_started_and_completed(self): """执行时广播 phase_started 和 phase_completed 事件""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") 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 @pytest.mark.asyncio async def test_emits_expert_step_and_result(self): """执行时广播 expert_step 和 expert_result 事件""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") calls = team._handoff_transport.send.call_args_list event_types = [c[0][1]["type"] for c in calls] assert "expert_step" in event_types assert "expert_result" in event_types @pytest.mark.asyncio async def test_phase_started_contains_depends_on(self): """phase_started 事件包含 depends_on 字段""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]}, ] ) team._experts["lead"].agent._llm_gateway = gateway await orchestrator.execute("依赖任务") calls = team._handoff_transport.send.call_args_list phase_started_events = [c[0][1] for c in calls if c[0][1].get("type") == "phase_started"] # Should have 2 phase_started events (A and B) assert len(phase_started_events) == 2 # B's phase_started should have depends_on with A's id phase_b_event = next(e for e in phase_started_events if e["phase_name"] == "B") assert len(phase_b_event["depends_on"]) == 1 @pytest.mark.asyncio async def test_phase_failed_event_on_failure(self): """阶段失败时广播 phase_failed 事件""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) # Make all agents fail to trigger phase_failed for expert in team._experts.values(): expert.agent.execute = AsyncMock(side_effect=RuntimeError("Execution failed")) result = await orchestrator.execute("失败任务") # Should fallback since all phases failed assert result["status"] == "fallback" # phase_failed should be emitted (or phase status is FAILED in plan) plan = result["plan"] assert all(ph.status == PhaseStatus.FAILED for ph in plan.phases) # ── LLM 任务分解测试 ────────────────────────────────────── class TestTaskDecomposition: """LLM 任务分解测试""" @pytest.mark.asyncio async def test_llm_decomposes_task_into_phases(self): """LLM 将任务分解为多个阶段""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "规划", "assigned_expert": "lead", "task_description": "设计架构", "depends_on": []}, {"name": "前端", "assigned_expert": "member1", "task_description": "实现UI", "depends_on": ["规划"]}, {"name": "后端", "assigned_expert": "member2", "task_description": "实现API", "depends_on": ["规划"]}, ] ) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("开发功能") assert result["status"] == "completed" plan = result["plan"] assert len(plan.phases) == 3 assert plan.phases[0].name == "规划" assert plan.phases[1].name == "前端" assert plan.phases[2].name == "后端" @pytest.mark.asyncio async def test_decomposition_fallback_to_single_phase(self): """LLM 不可用时回退到单个阶段""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) # No LLM gateway — should fall back to single phase result = await orchestrator.execute("测试任务") assert result["status"] == "completed" plan = result["plan"] assert len(plan.phases) == 1 @pytest.mark.asyncio async def test_llm_decomposition_invalid_json_falls_back(self): """LLM 返回无效 JSON 时回退到单阶段""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = AsyncMock() bad_response = MagicMock() bad_response.content = "这不是JSON" synth_response = MagicMock() synth_response.content = "综合结果" gateway.chat = AsyncMock(side_effect=[bad_response, synth_response]) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("测试任务") assert result["status"] == "completed" plan = result["plan"] assert len(plan.phases) == 1 @pytest.mark.asyncio async def test_parse_phases_valid_json(self): """_parse_phases 正确解析 JSON 数组""" content = json.dumps([ {"name": "A", "assigned_expert": "member1", "task_description": "任务A", "depends_on": []}, {"name": "B", "assigned_expert": "member2", "task_description": "任务B", "depends_on": ["A"]}, ]) phases = TeamOrchestrator._parse_phases(content, ["member1", "member2"], "lead") assert len(phases) == 2 assert phases[0].name == "A" assert phases[0].assigned_expert == "member1" assert phases[1].name == "B" assert phases[1].assigned_expert == "member2" # B depends on A assert len(phases[1].depends_on) == 1 assert phases[1].depends_on[0] == phases[0].id @pytest.mark.asyncio async def test_parse_phases_invalid_expert_falls_back_to_lead(self): """_parse_phases 对无效专家名回退到 lead""" content = json.dumps([ {"name": "A", "assigned_expert": "nonexistent", "task_description": "任务A", "depends_on": []}, ]) phases = TeamOrchestrator._parse_phases(content, ["member1"], "lead") assert len(phases) == 1 assert phases[0].assigned_expert == "lead" @pytest.mark.asyncio async def test_parse_phases_invalid_json_returns_empty(self): """_parse_phases 对无效 JSON 返回空列表""" phases = TeamOrchestrator._parse_phases("not json at all", ["member1"], "lead") assert phases == [] @pytest.mark.asyncio async def test_parse_phases_empty_name_skipped(self): """_parse_phases 跳过空名称的阶段""" content = json.dumps([ {"name": "", "assigned_expert": "member1", "task_description": "任务A", "depends_on": []}, {"name": "B", "assigned_expert": "member1", "task_description": "任务B", "depends_on": []}, ]) phases = TeamOrchestrator._parse_phases(content, ["member1"], "lead") assert len(phases) == 1 assert phases[0].name == "B" @pytest.mark.asyncio async def test_parse_phases_resolves_depends_on_by_name(self): """_parse_phases 通过名称解析 depends_on 为 ID""" content = json.dumps([ {"name": "规划", "assigned_expert": "lead", "task_description": "设计", "depends_on": []}, {"name": "实现", "assigned_expert": "member1", "task_description": "编码", "depends_on": ["规划"]}, ]) phases = TeamOrchestrator._parse_phases(content, ["lead", "member1"], "lead") assert len(phases) == 2 # 实现 depends on 规划 assert phases[1].depends_on == [phases[0].id] @pytest.mark.asyncio async def test_parse_phases_unknown_dependency_ignored(self): """_parse_phases 对未知依赖名称忽略""" content = json.dumps([ {"name": "A", "assigned_expert": "lead", "task_description": "任务A", "depends_on": ["unknown_phase"]}, ]) phases = TeamOrchestrator._parse_phases(content, ["lead"], "lead") assert len(phases) == 1 assert len(phases[0].depends_on) == 0 # unknown dependency ignored # ── 阶段执行测试 ────────────────────────────────────────── class TestPhaseExecution: """阶段执行测试""" @pytest.mark.asyncio async def test_phase_execution_calls_agent_execute(self): """阶段执行调用 agent.execute()""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") # Lead expert's agent should have been called team._experts["lead"].agent.execute.assert_awaited() @pytest.mark.asyncio async def test_phase_marks_completed(self): """阶段执行后状态标记为 COMPLETED""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) result = await orchestrator.execute("测试任务") plan = result["plan"] for ph in plan.phases: assert ph.status == PhaseStatus.COMPLETED assert ph.result is not None @pytest.mark.asyncio async def test_phase_with_invalid_expert_falls_back_to_lead(self): """阶段分配的专家不可用时回退到 lead""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "nonexistent", "task_description": "任务A", "depends_on": []}, ] ) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("测试任务") assert result["status"] == "completed" plan = result["plan"] # The phase should have been reassigned to lead assert plan.phases[0].assigned_expert == "lead" @pytest.mark.asyncio async def test_shared_workspace_passing(self): """阶段 A 的输出写入 SharedWorkspace,阶段 B 能读取""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]}, ] ) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("依赖任务") assert result["status"] == "completed" # Verify workspace.write was called for each phase workspace = team.workspace # Check that phase outputs were written plan = result["plan"] for ph in plan.phases: key = f"{plan.id}/phase/{ph.id}/output" data = await workspace.read(key) assert data is not None assert "value" in data @pytest.mark.asyncio async def test_context_isolation_with_pool(self): """使用 AgentPool 时创建独立 agent 实例(上下文隔离)""" pool = _make_mock_pool() team = _make_team_with_experts(pool=pool) orchestrator = TeamOrchestrator(team) await orchestrator.execute("测试任务") # Pool.create_agent should have been called for context isolation pool.create_agent.assert_awaited() # Pool.remove_agent should have been called for cleanup pool.remove_agent.assert_awaited() @pytest.mark.asyncio async def test_context_isolation_fallback_without_pool(self): """无 AgentPool 时使用 expert 的现有 agent""" team = _make_team_with_experts() # No pool orchestrator = TeamOrchestrator(team) result = await orchestrator.execute("测试任务") # Should still complete successfully using expert's existing agent assert result["status"] == "completed" team._experts["lead"].agent.execute.assert_awaited() # ── 阶段失败与依赖传播测试 ──────────────────────────────── class TestPhaseFailure: """阶段失败与依赖传播测试""" @pytest.mark.asyncio async def test_phase_failure_marks_dependents_failed(self): """阶段 B 失败时,依赖 B 的阶段 C 标记为 FAILED""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "lead", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member1", "task_description": "阶段B", "depends_on": ["A"]}, {"name": "C", "assigned_expert": "member2", "task_description": "阶段C", "depends_on": ["B"]}, ] ) team._experts["lead"].agent._llm_gateway = gateway # Make member1's agent fail (phase B) team._experts["member1"].agent.execute = AsyncMock(side_effect=RuntimeError("B failed")) result = await orchestrator.execute("失败传播任务") # Should still complete (phase A succeeded) assert result["status"] == "completed" plan = result["plan"] # Phase A should be completed phase_a = next(ph for ph in plan.phases if ph.name == "A") assert phase_a.status == PhaseStatus.COMPLETED # Phase B should be failed phase_b = next(ph for ph in plan.phases if ph.name == "B") assert phase_b.status == PhaseStatus.FAILED # Phase C should be failed (dependency B failed) phase_c = next(ph for ph in plan.phases if ph.name == "C") assert phase_c.status == PhaseStatus.FAILED @pytest.mark.asyncio async def test_all_phases_fail_triggers_fallback(self): """所有阶段失败时触发 fallback""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) # Make all agents fail for expert in team._experts.values(): expert.agent.execute = AsyncMock(side_effect=RuntimeError("Execution failed")) result = await orchestrator.execute("全失败任务") assert result["status"] == "fallback" assert result["plan"].status == PlanStatus.FALLBACK @pytest.mark.asyncio async def test_fallback_uses_lead_expert(self): """回退使用 lead expert 执行原始任务""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) call_count = 0 async def mock_execute(task_msg): nonlocal call_count call_count += 1 if task_msg.task_type == "team_phase": raise RuntimeError("Phase failed") # Fallback succeeds return TaskResult( task_id=task_msg.task_id, agent_name="lead", status=TaskStatus.COMPLETED.value, output_data={"content": "Fallback result"}, error_message=None, started_at=None, completed_at=None, ) team._experts["lead"].agent.execute = AsyncMock(side_effect=mock_execute) result = await orchestrator.execute("测试任务") assert result["status"] == "fallback" assert result["result"]["content"] == "Fallback result" @pytest.mark.asyncio async def test_no_active_experts_returns_failed(self): """没有活跃专家时返回 failed""" team = _make_team_with_experts() for expert in team._experts.values(): expert.is_active = False orchestrator = TeamOrchestrator(team) result = await orchestrator.execute("测试任务") assert result["status"] == "failed" assert "error" in result # ── 循环依赖检测测试 ────────────────────────────────────── class TestCircularDependency: """循环依赖检测测试""" @pytest.mark.asyncio async def test_circular_dependency_triggers_fallback(self): """A→B→A 的循环依赖触发 fallback""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) # Manually create a plan with circular dependency # We need to mock _decompose_task to return phases with circular dep phase_a = PlanPhase(name="A", assigned_expert="lead", task_description="A") phase_b = PlanPhase(name="B", assigned_expert="member1", task_description="B") phase_a.depends_on = [phase_b.id] phase_b.depends_on = [phase_a.id] with patch.object( TeamOrchestrator, "_decompose_task", return_value=[phase_a, phase_b], ): result = await orchestrator.execute("循环依赖任务") # Should fallback due to ValueError from topological_sort assert result["status"] == "fallback" assert result["plan"].status == PlanStatus.FALLBACK # ── 结果综合测试 ────────────────────────────────────────── class TestResultSynthesis: """结果综合测试""" @pytest.mark.asyncio async def test_synthesize_single_result(self): """单个阶段结果直接返回""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) result = await orchestrator.execute("测试任务") assert result["status"] == "completed" final = result["result"] assert "content" in final assert final["strategy"] == "best" assert final["phases_completed"] == 1 @pytest.mark.asyncio async def test_synthesize_multiple_results_with_llm(self): """多个阶段结果通过 LLM 综合""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = _make_mock_llm_gateway( phases=[ {"name": "A", "assigned_expert": "member1", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member2", "task_description": "阶段B", "depends_on": []}, ], synthesis_content="综合结果", ) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("复杂任务") assert result["status"] == "completed" final = result["result"] assert final["content"] == "综合结果" assert final["strategy"] == "best" assert final["phases_completed"] == 2 @pytest.mark.asyncio async def test_synthesize_without_llm_concatenates(self): """无 LLM 时拼接所有结果""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) gateway = AsyncMock() decomp_response = MagicMock() decomp_response.content = json.dumps([ {"name": "A", "assigned_expert": "member1", "task_description": "阶段A", "depends_on": []}, {"name": "B", "assigned_expert": "member2", "task_description": "阶段B", "depends_on": []}, ]) # Synthesis call raises to force concatenation fallback gateway.chat = AsyncMock( side_effect=[decomp_response, RuntimeError("LLM unavailable")] ) team._experts["lead"].agent._llm_gateway = gateway result = await orchestrator.execute("复杂任务") assert result["status"] == "completed" final = result["result"] assert "content" in final # Should contain both results concatenated assert "Result from member1" in final["content"] assert "Result from member2" in final["content"] # ── 模型路由测试 ────────────────────────────────────────── class TestModelRouting: """模型路由测试(_get_model)""" def test_get_model_default_when_no_llm_config(self): """无 llm 配置时返回 'default'""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) model = orchestrator._get_model() assert model == "default" def test_get_model_from_expert_config(self): """从 expert.config.llm 读取模型名""" team = _make_team_with_experts() # Set llm config on lead expert team._experts["lead"].config.llm = {"model": "gpt-4", "temperature": 0.7} orchestrator = TeamOrchestrator(team) model = orchestrator._get_model() assert model == "gpt-4" def test_get_model_falls_back_to_default_when_no_model_key(self): """llm 配置中无 model 键时回退到 'default'""" team = _make_team_with_experts() team._experts["lead"].config.llm = {"temperature": 0.5} orchestrator = TeamOrchestrator(team) model = orchestrator._get_model() assert model == "default" def test_get_model_uses_specific_expert(self): """_get_model 使用指定 expert 的配置""" team = _make_team_with_experts() team._experts["member1"].config.llm = {"model": "claude-3"} orchestrator = TeamOrchestrator(team) model = orchestrator._get_model(team._experts["member1"]) assert model == "claude-3" # ── TeamStatus.PLANNING 状态流转测试 ────────────────────── class TestTeamStatusPlanning: """TeamStatus.PLANNING 状态流转测试(KTD6)""" def test_team_status_planning_exists(self): """TeamStatus.PLANNING 枚举值存在""" assert TeamStatus.PLANNING == "planning" @pytest.mark.asyncio async def test_execute_transitions_through_planning(self): """执行时经过 PLANNING 状态""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) # Track status transitions statuses_seen: list[str] = [] original_set_status = team.set_status def tracking_set_status(status: TeamStatus) -> None: statuses_seen.append(status.value) original_set_status(status) team.set_status = tracking_set_status await orchestrator.execute("测试任务") # Should have transitioned through PLANNING assert "planning" in statuses_seen # Should end at COMPLETED assert statuses_seen[-1] == "completed" @pytest.mark.asyncio async def test_execute_transitions_to_executing_after_planning(self): """PLANNING 后转为 EXECUTING""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) statuses_seen: list[str] = [] original_set_status = team.set_status def tracking_set_status(status: TeamStatus) -> None: statuses_seen.append(status.value) original_set_status(status) team.set_status = tracking_set_status await orchestrator.execute("测试任务") # PLANNING should come before EXECUTING planning_idx = statuses_seen.index("planning") executing_idx = statuses_seen.index("executing") assert planning_idx < executing_idx # ── 事件广播基础设施测试 ────────────────────────────────── 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() 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_broadcast_event_handles_transport_error(self): """handoff_transport 发送失败时不影响执行""" team = _make_team_with_experts() team._handoff_transport.send = AsyncMock(side_effect=RuntimeError("Transport error")) orchestrator = TeamOrchestrator(team) # Should not raise await orchestrator._broadcast_event("test_event", {"key": "value"}) # ── LLM Gateway 测试 ────────────────────────────────────── class TestLLMGateway: """LLM Gateway 获取测试""" def test_get_llm_gateway_from_lead(self): """从 lead expert 获取 LLM gateway""" team = _make_team_with_experts() gateway = MagicMock() team._experts["lead"].agent._llm_gateway = gateway orchestrator = TeamOrchestrator(team) result = orchestrator._get_llm_gateway() assert result is gateway def test_get_llm_gateway_no_gateway(self): """没有 LLM gateway 时返回 None""" team = _make_team_with_experts() orchestrator = TeamOrchestrator(team) result = orchestrator._get_llm_gateway() assert result is None def test_get_llm_gateway_fallback_to_active_expert(self): """lead 没有 gateway 时从其他活跃专家获取""" team = _make_team_with_experts() gateway = MagicMock() team._experts["member1"].agent._llm_gateway = gateway orchestrator = TeamOrchestrator(team) result = orchestrator._get_llm_gateway() assert result is gateway