"""Router 单元测试 — 三种路由模式 + 校验 + fallback""" import pytest from unittest.mock import MagicMock, patch from src.daemon.router import ( AgentRouter, AgentProfile, LLMDriver, RouteDecision, KNOWN_CAPABILITIES, ) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def profiles(): return { "zhangfei-dev": AgentProfile( agent_id="zhangfei-dev", capabilities=["coding", "implementation", "scripting"], can_review=False, max_concurrent=1, ), "simayi-challenger": AgentProfile( agent_id="simayi-challenger", capabilities=["review", "quality_check", "debate"], can_review=True, max_concurrent=2, ), "pangtong-fujunshi": AgentProfile( agent_id="pangtong-fujunshi", capabilities=["planning", "coordination", "escalation", "strategy"], can_review=True, is_fallback=True, max_concurrent=3, ), } @pytest.fixture def router(profiles): return AgentRouter(agent_profiles=profiles) def make_task(**overrides): base = { "id": "t1", "title": "Test Task", "status": "pending", "assignee": "zhangfei-dev", "current_agent": None, "next_capability": None, "task_type": "coding", "description": "Write some code", } base.update(overrides) return base # --------------------------------------------------------------------------- # T1: 确定性快速路径 # --------------------------------------------------------------------------- class TestDeterministic: def test_local_action(self, router): """机械检查 → daemon""" d = router.route(make_task(), action_type="L1_guardrail") assert d.agent_id == "daemon" assert d.mode == "deterministic" def test_format_check_local(self, router): d = router.route(make_task(), action_type="format_check") assert d.agent_id == "daemon" def test_retry_same_agent(self, router): """retry → 原执行者""" d = router.route(make_task(current_agent="zhangfei-dev"), action_type="retry") assert d.agent_id == "zhangfei-dev" assert d.mode == "deterministic" def test_direct_assignee(self, router): """有 assignee 且非生命周期流转 → 直接用""" d = router.route(make_task(), action_type="execute") assert d.agent_id == "zhangfei-dev" assert d.mode == "deterministic" # --------------------------------------------------------------------------- # T2: Mode B — Agent 声明式交接 # --------------------------------------------------------------------------- class TestAgentHandoff: def test_handoff_review(self, router): """张飞说需要 review → 匹配司马懿""" d = router.route(make_task( current_agent="zhangfei-dev", next_capability="review", )) assert d.agent_id == "simayi-challenger" assert d.mode == "agent_handoff" assert "review" in d.reason def test_handoff_excludes_current(self, router): """交接排除当前执行者(不能交给自己)""" d = router.route(make_task( current_agent="simayi-challenger", next_capability="review", )) # simayi 被排除,只剩 pangtong(can_review=True 且有 review 能力) assert d.agent_id != "simayi-challenger" def test_handoff_invalid_capability_ignored(self, router): """BUG-2: 非法 next_capability 被忽略,不进路由""" d = router.route(make_task( current_agent="zhangfei-dev", next_capability="nonexistent_capability", ), action_type="execute") # 不应该走 handoff,应该走 assignee 直派或其他 assert d.mode != "agent_handoff" def test_handoff_known_but_no_match(self): """合法 capability 但无匹配 Agent → 降级""" router2 = AgentRouter(agent_profiles={ "zhangfei-dev": AgentProfile( agent_id="zhangfei-dev", capabilities=["coding"], ), }) d = router2.route(make_task( assignee=None, # 无 assignee,不会走快速路径 4 current_agent="zhangfei-dev", next_capability="review", # 合法但在 profiles 中无人匹配 )) # 无 LLM driver、无 assignee → fallback 庞统 assert d.agent_id == "pangtong-fujunshi" assert d.mode == "fallback" # --------------------------------------------------------------------------- # T3: 生命周期流转 # --------------------------------------------------------------------------- class TestLifecycle: def test_review_action(self, router): """review action_type → 查 review 能力,排除 current_agent""" d = router.route(make_task( current_agent="zhangfei-dev", ), action_type="review") assert d.agent_id == "simayi-challenger" assert d.mode == "deterministic" def test_review_excludes_executor(self, router): """review 排除执行者(张飞不能审张飞)""" d = router.route(make_task( current_agent="zhangfei-dev", assignee="zhangfei-dev", ), action_type="review") assert d.agent_id != "zhangfei-dev" assert d.agent_id == "simayi-challenger" # --------------------------------------------------------------------------- # T4: Mode A — LLM 路由 # --------------------------------------------------------------------------- class TestLLMRoute: def test_llm_returns_valid(self, router): """LLM 返回合法 Agent""" mock_driver = MagicMock(spec=LLMDriver) mock_driver.route.return_value = RouteDecision( agent_id="simayi-challenger", reason="LLM selected reviewer", mode="llm_route", confidence=0.9, model="zhipu/glm-5.1", latency_ms=1200, ) router.llm_driver = mock_driver # 无 assignee、无 next_capability、非 lifecycle → 走 LLM d = router.route(make_task( assignee=None, current_agent=None, next_capability=None, )) assert d.agent_id == "simayi-challenger" assert d.mode == "llm_route" assert d.confidence == 0.9 def test_llm_invalid_agent_fallback(self, router): """LLM 返回不存在的 Agent → fallback""" mock_driver = MagicMock(spec=LLMDriver) mock_driver.route.return_value = RouteDecision( agent_id="nonexistent-agent", reason="Bad pick", mode="llm_route", confidence=0.8, latency_ms=500, ) router.llm_driver = mock_driver d = router.route(make_task(assignee=None)) assert d.agent_id == "pangtong-fujunshi" assert d.mode == "fallback" def test_llm_low_confidence_fallback(self, router): """LLM 置信度低于阈值 → fallback""" mock_driver = MagicMock(spec=LLMDriver) mock_driver.route.return_value = RouteDecision( agent_id="simayi-challenger", reason="Not sure", mode="llm_route", confidence=0.5, latency_ms=300, ) router.llm_driver = mock_driver d = router.route(make_task(assignee=None)) assert d.agent_id == "pangtong-fujunshi" assert d.mode == "fallback" assert d.confidence == 0.5 # --------------------------------------------------------------------------- # T5: Fallback # --------------------------------------------------------------------------- class TestFallback: def test_no_llm_no_match(self, router): """无 LLM、无匹配 → fallback 庞统""" d = router.route(make_task( assignee=None, current_agent=None, next_capability=None, task_type="unknown_type", ), action_type="") assert d.agent_id == "pangtong-fujunshi" assert d.mode == "fallback" # --------------------------------------------------------------------------- # T6: Capability 校验 # --------------------------------------------------------------------------- class TestCapabilityValidation: def test_known_capabilities_populated(self, router): """profiles 自动填充 known capabilities""" assert "coding" in router._known_capabilities assert "review" in router._known_capabilities assert "escalation" in router._known_capabilities def test_validate_known(self, router): assert router._validate_capability("coding") is True assert router._validate_capability("review") is True def test_validate_unknown(self, router): assert router._validate_capability("administration") is False assert router._validate_capability("hack") is False def test_known_capabilities_set(self): """KNOWN_CAPABILITIES 包含所有预期能力""" assert "coding" in KNOWN_CAPABILITIES assert "review" in KNOWN_CAPABILITIES assert "escalation" in KNOWN_CAPABILITIES # --------------------------------------------------------------------------- # T7: Latency 记录 # --------------------------------------------------------------------------- class TestLatency: def test_deterministic_has_latency(self, router): d = router.route(make_task(), action_type="L1_guardrail") assert d.latency_ms >= 0 def test_handoff_has_latency(self, router): d = router.route(make_task( current_agent="zhangfei-dev", next_capability="review", )) assert d.latency_ms >= 0