Files
sanguo_moziplus_v2/tests/test_router.py
T
2026-06-03 08:03:32 +08:00

273 lines
9.8 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.
"""Router 单元测试 — 三种路由模式 + 校验 + fallback"""
import pytest
from unittest.mock import MagicMock, patch
from src.daemon.router import (
AgentRouter, AgentProfile, 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 被排除,只剩 pangtongcan_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 路由 (v3.0: LLMDriver 已移除,模糊路由 delegate 庞统)
# ---------------------------------------------------------------------------
@pytest.mark.skip(reason="v3.0 removed LLMDriver, fuzzy routing delegates to pangtong")
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