from __future__ import annotations from svc.llm.chat_models import LLMResponse from runtime.router import decide_route from runtime.types import StandardMessage, normalize_interaction_mode, normalize_requested_specialist def _msg(text: str, attachments: list[dict] | None = None) -> StandardMessage: return StandardMessage( session_id="s1", tenant_id="t1", user_id="u1", role="user", channel="admin_chat", text=text, attachments=attachments or [], metadata={}, ) def test_router_async_for_multi_step_send_flow() -> None: d = decide_route(_msg("帮我总结并发送到项目群")) assert d.mode == "async_task" assert d.reason == "multi_step_send_flow" def test_router_sync_for_short_default_message() -> None: d = decide_route(_msg("你好,今天怎么样")) assert d.mode == "sync_direct" assert d.reason == "default_sync" class _RouterJsonModel: def __init__(self, content: str) -> None: self._content = content def chat(self, messages, tools, *, on_token=None): return LLMResponse(content=self._content, tool_calls=[]) def test_router_llm_json_mode_uses_model() -> None: class Store: def get_setting(self, key: str) -> str: if key == "AIA_OCLAW_ROUTER_MODE": return "llm_json" return "" d = decide_route( _msg("short"), store=Store(), model=_RouterJsonModel('{"mode":"async_task","reason":"user asked for batch"}'), ) assert d.mode == "async_task" assert "batch" in d.reason def test_router_llm_json_invalid_json_falls_back_to_rule() -> None: class Store: def get_setting(self, key: str) -> str: if key == "AIA_OCLAW_ROUTER_MODE": return "llm_json" return "" d = decide_route(_msg("你好,今天怎么样"), store=Store(), model=_RouterJsonModel("not json")) assert d.mode == "sync_direct" assert d.reason == "default_sync" def test_router_llm_json_tolerates_extra_dynamic_agent_fields() -> None: class Store: def get_setting(self, key: str) -> str: if key == "AIA_OCLAW_ROUTER_MODE": return "llm_json" return "" d = decide_route( _msg("plan this"), store=Store(), model=_RouterJsonModel('{"mode":"sync_direct","reason":"ok","dynamic_agent":{"name":"x"}}'), ) assert d.mode == "sync_direct" assert d.reason == "ok" def test_router_skill_signal_from_metadata() -> None: msg = StandardMessage( session_id="s1", tenant_id="t1", user_id="u1", role="user", channel="admin_chat", text="你好", attachments=[], metadata={"skills_total": 4}, ) d = decide_route(msg) assert d.skill_signal == "skills=4" def test_interaction_mode_normalization_is_expert_only() -> None: assert normalize_interaction_mode("comprehensive") == "expert" assert normalize_interaction_mode("expert") == "expert" assert normalize_interaction_mode("specialist") == "expert" assert normalize_interaction_mode("composite") == "expert" assert normalize_interaction_mode("") == "expert" assert normalize_interaction_mode(None) == "expert" def test_requested_specialist_normalization_defaults_to_generalist() -> None: assert normalize_requested_specialist("ops") == "ops" # Removed specialists map to generalist. assert normalize_requested_specialist("stock") == "generalist" assert normalize_requested_specialist("image") == "generalist" assert normalize_requested_specialist("video") == "generalist" assert normalize_requested_specialist("memory") == "memory" assert normalize_requested_specialist("unknown") == "generalist" def test_router_removed_video_expert_uses_normal_routing() -> None: long_text = "x" * 150 msg = StandardMessage( session_id="s1", tenant_id="t1", user_id="u1", role="user", channel="admin_chat", text=long_text, attachments=[{"type": "image_ref", "attachment_id": "a" * 64}], metadata={"interaction_mode": "expert", "selected_specialist": "video"}, ) d = decide_route(msg) assert d.requested_specialist == "generalist" assert d.reason != "video_expert_legacy_lane" def test_router_carries_interaction_mode_and_requested_specialist() -> None: msg = StandardMessage( session_id="s1", tenant_id="t1", user_id="u1", role="user", channel="admin_chat", text="你好", attachments=[], metadata={"interaction_mode": "expert", "selected_specialist": "ops"}, ) d = decide_route(msg) assert d.interaction_mode == "expert" assert d.requested_specialist == "ops"