"""CLI 多 Agent 入口 + 辩论支持单元测试 (U6)""" from __future__ import annotations import io from unittest.mock import MagicMock, patch import pytest from rich.console import Console from agentkit.experts.router import ExpertTeamRouter from agentkit.experts.team import ExpertTeam # --------------------------------------------------------------------------- # @team 前缀路由测试 # --------------------------------------------------------------------------- class TestTeamPrefixRouting: """@team 前缀路由测试""" def test_team_prefix_matched(self): """@team 前缀被 ExpertTeamRouter 识别""" router = ExpertTeamRouter() result = router.resolve("@team 开发用户登录功能") assert result.matched is True assert result.task_content == "开发用户登录功能" def test_team_prefix_with_template(self): """@team:dev_team 模板被识别""" router = ExpertTeamRouter() result = router.resolve("@team:dev_team 开发API") assert result.matched is True assert result.task_content == "开发API" def test_non_team_input_not_matched(self): """非 @team 输入不被匹配""" router = ExpertTeamRouter() result = router.resolve("你好") assert result.matched is False def test_team_prefix_alone_matched(self): """@team 单独出现也被匹配(task_content 回退为完整输入)""" router = ExpertTeamRouter() result = router.resolve("@team") assert result.matched is True # --------------------------------------------------------------------------- # _print_help 文档测试 # --------------------------------------------------------------------------- class TestPrintHelp: """_print_help 包含 @team 文档测试""" def test_help_includes_team_docs(self): """帮助文本包含 @team 说明""" from agentkit.cli.chat import _print_help captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _print_help() text = captured.getvalue() assert "@team" in text assert "/debate" in text assert "/stop" in text assert "专家团" in text def test_help_includes_intervention_section(self): """帮助文本包含干预说明""" from agentkit.cli.chat import _print_help captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _print_help() text = captured.getvalue() assert "Interventions" in text or "干预" in text # --------------------------------------------------------------------------- # _execute_team_cli 函数测试 # --------------------------------------------------------------------------- class TestExecuteTeamCli: """_execute_team_cli 函数测试""" @pytest.mark.asyncio async def test_returns_false_for_non_team_input(self): """非 @team 输入返回 False""" from agentkit.cli.chat import _execute_team_cli gateway = MagicMock() pool = MagicMock() registry = MagicMock() result = await _execute_team_cli("你好", gateway, pool, registry) assert result is False @pytest.mark.asyncio async def test_returns_true_for_team_without_task(self): """@team 无任务描述返回 True(已处理,提示用法)""" from agentkit.cli.chat import _execute_team_cli gateway = MagicMock() pool = MagicMock() registry = MagicMock() with patch.object(ExpertTeamRouter, "resolve") as mock_resolve: mock_result = MagicMock() mock_result.matched = True mock_result.task_content = "" mock_resolve.return_value = mock_result result = await _execute_team_cli("@team", gateway, pool, registry) assert result is True @pytest.mark.asyncio async def test_returns_true_when_experts_unresolvable(self): """@team 有任务但无法解析专家时返回 True(错误提示)""" from agentkit.cli.chat import _execute_team_cli gateway = MagicMock() pool = MagicMock() registry = MagicMock() with ( patch.object(ExpertTeamRouter, "resolve") as mock_resolve, patch.object(ExpertTeamRouter, "resolve_expert_configs") as mock_configs, ): mock_result = MagicMock() mock_result.matched = True mock_result.task_content = "开发功能" mock_result.specified_experts = ["nonexistent"] mock_resolve.return_value = mock_result mock_configs.return_value = [] result = await _execute_team_cli("@team:nonexistent 开发功能", gateway, pool, registry) assert result is True # --------------------------------------------------------------------------- # 干预命令支持测试 # --------------------------------------------------------------------------- class TestInterventionSupport: """干预命令基础设施测试""" def test_team_has_broadcast_user_message(self): """ExpertTeam 有 broadcast_user_message 方法(干预广播基础)""" assert hasattr(ExpertTeam, "broadcast_user_message") def test_help_lists_debate_command(self): """帮助文本列出 /debate 命令""" from agentkit.cli.chat import _print_help captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _print_help() text = captured.getvalue() assert "/debate" in text assert "辩论" in text def test_help_lists_stop_command(self): """帮助文本列出 /stop 命令""" from agentkit.cli.chat import _print_help captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _print_help() text = captured.getvalue() assert "/stop" in text assert "终止" in text # --------------------------------------------------------------------------- # U6: 项目经理模式协同事件渲染测试 # --------------------------------------------------------------------------- class TestPMCollaborationRendering: """U6: 项目经理模式协同事件渲染测试""" def _capture_render(self, message: dict) -> str: """辅助:渲染 PM 事件并捕获输出。""" from agentkit.cli.chat import _render_pm_collaboration_event captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _render_pm_collaboration_event(message) return captured.getvalue() def test_collaboration_contract_defined_renders_panel(self): """collaboration_contract_defined 事件渲染为 Panel""" message = { "type": "collaboration_contract_defined", "contracts": [ { "from_expert": "backend", "to_expert": "frontend", "content_description": "API 定义", "status": "pending", }, ], } text = self._capture_render(message) assert "协作契约" in text assert "backend" in text assert "frontend" in text assert "API 定义" in text def test_collaboration_contract_defined_empty_contracts(self): """collaboration_contract_defined 空契约列表不产生输出""" message = {"type": "collaboration_contract_defined", "contracts": []} text = self._capture_render(message) assert text == "" def test_collaboration_notice_renders_colored_text(self): """collaboration_notice 事件渲染为带颜色的文本""" message = { "type": "collaboration_notice", "from_expert": "backend", "to_expert": "frontend", "content_description": "API 定义已就绪", } text = self._capture_render(message) assert "backend" in text assert "frontend" in text assert "API 定义已就绪" in text def test_review_result_passed_renders_green(self): """review_result (passed=True) 渲染为绿色""" message = { "type": "review_result", "phase_name": "后端开发", "passed": True, "feedback": "", "expert": "backend_engineer", } text = self._capture_render(message) assert "验收通过" in text assert "后端开发" in text def test_review_result_failed_renders_red(self): """review_result (passed=False) 渲染为红色""" message = { "type": "review_result", "phase_name": "后端开发", "passed": False, "feedback": "API 缺少错误处理", "expert": "backend_engineer", "rework_count": 1, } text = self._capture_render(message) assert "验收未通过" in text assert "API 缺少错误处理" in text assert "返工次数" in text def test_risk_flagged_renders_yellow_panel(self): """risk_flagged 事件渲染为黄色 Panel""" message = { "type": "risk_flagged", "expert": "backend_engineer", "risk_description": "数据库连接池可能不足", "phase_name": "后端开发", } text = self._capture_render(message) assert "风险标记" in text assert "数据库连接池可能不足" in text assert "backend_engineer" in text def test_missing_data_graceful_degradation(self): """事件数据缺失时优雅降级""" # collaboration_notice 缺少字段 → 回退到 "?" text = self._capture_render({"type": "collaboration_notice"}) assert "?" in text # review_result 缺少字段 → 仍渲染(默认 failed=红色) text = self._capture_render({"type": "review_result"}) assert "验收" in text # risk_flagged 缺少字段 → 仍渲染 text = self._capture_render({"type": "risk_flagged"}) assert "风险标记" in text def test_unhandled_event_returns_false(self): """非 PM 事件返回 False""" from agentkit.cli.chat import _render_pm_collaboration_event result = _render_pm_collaboration_event({"type": "team_formed"}) assert result is False def test_pm_event_returns_true_when_handled(self): """PM 事件返回 True""" from agentkit.cli.chat import _render_pm_collaboration_event for etype in ( "collaboration_contract_defined", "collaboration_notice", "review_result", "risk_flagged", ): result = _render_pm_collaboration_event({"type": etype}) assert result is True, f"{etype} should return True" class TestPrintHelpPMMode: """_print_help 包含项目经理模式说明测试""" def test_help_includes_pm_mode(self): """帮助文本包含项目经理模式说明""" from agentkit.cli.chat import _print_help captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _print_help() text = captured.getvalue() assert "项目经理" in text def test_help_includes_collaboration_events(self): """帮助文本包含协同事件说明""" from agentkit.cli.chat import _print_help captured = io.StringIO() console = Console(file=captured, width=120) with patch( "agentkit.cli.chat.rprint", side_effect=lambda *a, **kw: console.print(*a, **kw), ): _print_help() text = captured.getvalue() assert "协作契约" in text assert "验收结果" in text assert "风险标记" in text