""" Unit tests for Mode Router. [AC-AISVC-RES-09,10,11,12,13,14] Tests for mode routing, complexity analysis, and fallback. """ import pytest from unittest.mock import AsyncMock, MagicMock, patch from app.services.retrieval.routing_config import ( RagRuntimeMode, RoutingConfig, StrategyContext, ) from app.services.retrieval.mode_router import ( ComplexityAnalyzer, ModeRouteResult, ModeRouter, get_mode_router, reset_mode_router, ) class TestComplexityAnalyzer: """[AC-AISVC-RES-12,13] Tests for ComplexityAnalyzer.""" @pytest.fixture def analyzer(self): return ComplexityAnalyzer() def test_analyze_empty_query(self, analyzer): """Empty query should have zero complexity.""" score = analyzer.analyze("") assert score == 0.0 def test_analyze_short_query(self, analyzer): """Short query should have low complexity.""" score = analyzer.analyze("简单问题") assert score < 0.3 def test_analyze_long_query(self, analyzer): """Long query should have higher complexity.""" long_query = "这是一个很长的问题" * 20 score = analyzer.analyze(long_query) assert score >= 0.3 def test_analyze_multiple_conditions(self, analyzer): """[AC-AISVC-RES-13] Query with multiple conditions should have high complexity.""" query = "查询订单状态和物流信息以及退款进度" score = analyzer.analyze(query) assert score >= 0.2 def test_analyze_reasoning_patterns(self, analyzer): """Query with reasoning patterns should have higher complexity.""" query = "为什么订单会被取消?原因是什么?" score = analyzer.analyze(query) assert score >= 0.15 def test_analyze_cross_domain_patterns(self, analyzer): """Cross-domain query should have higher complexity.""" query = "汇总所有部门的销售数据" score = analyzer.analyze(query) assert score >= 0.15 def test_analyze_multiple_questions(self, analyzer): """Multiple questions should increase complexity.""" query = "什么是价格?如何购买?" score = analyzer.analyze(query) assert score >= 0.1 class TestModeRouteResult: """Tests for ModeRouteResult.""" def test_result_creation(self): """Should create result with all fields.""" result = ModeRouteResult( mode=RagRuntimeMode.REACT, confidence=0.5, complexity_score=0.7, should_fallback_to_react=True, fallback_reason="Low confidence", diagnostics={"key": "value"}, ) assert result.mode == RagRuntimeMode.REACT assert result.confidence == 0.5 assert result.complexity_score == 0.7 assert result.should_fallback_to_react is True assert result.fallback_reason == "Low confidence" assert result.diagnostics == {"key": "value"} def test_result_defaults(self): """Should create result with default values.""" result = ModeRouteResult( mode=RagRuntimeMode.DIRECT, confidence=0.8, complexity_score=0.2, ) assert result.should_fallback_to_react is False assert result.fallback_reason is None assert result.diagnostics == {} class TestModeRouter: """[AC-AISVC-RES-09,10,11,12,13,14] Tests for ModeRouter.""" @pytest.fixture def router(self): reset_mode_router() return ModeRouter() def test_initial_config(self, router): """Should initialize with default configuration.""" assert router.config.rag_runtime_mode == RagRuntimeMode.AUTO def test_route_direct_mode(self, router): """[AC-AISVC-RES-09] Should route to direct mode when configured.""" router._config.rag_runtime_mode = RagRuntimeMode.DIRECT ctx = StrategyContext(tenant_id="tenant_a", query="Test query") result = router.route(ctx) assert result.mode == RagRuntimeMode.DIRECT def test_route_react_mode(self, router): """[AC-AISVC-RES-10] Should route to react mode when configured.""" router._config.rag_runtime_mode = RagRuntimeMode.REACT ctx = StrategyContext(tenant_id="tenant_a", query="Test query") result = router.route(ctx) assert result.mode == RagRuntimeMode.REACT def test_route_auto_mode_direct_conditions(self, router): """[AC-AISVC-RES-11,12] Auto mode should select direct for simple queries.""" router._config.rag_runtime_mode = RagRuntimeMode.AUTO ctx = StrategyContext( tenant_id="tenant_a", query="简单问题", metadata_confidence=0.9, complexity_score=0.1, ) result = router.route(ctx) assert result.mode == RagRuntimeMode.DIRECT def test_route_auto_mode_react_low_confidence(self, router): """[AC-AISVC-RES-11,12] Auto mode should select react for low confidence.""" router._config.rag_runtime_mode = RagRuntimeMode.AUTO router._config.react_trigger_confidence_threshold = 0.6 ctx = StrategyContext( tenant_id="tenant_a", query="Test query", metadata_confidence=0.4, complexity_score=0.2, ) result = router.route(ctx) assert result.mode == RagRuntimeMode.REACT def test_route_auto_mode_react_high_complexity(self, router): """[AC-AISVC-RES-11,13] Auto mode should select react for high complexity.""" router._config.rag_runtime_mode = RagRuntimeMode.AUTO router._config.react_trigger_complexity_score = 0.5 ctx = StrategyContext( tenant_id="tenant_a", query="Test query", metadata_confidence=0.8, complexity_score=0.7, ) result = router.route(ctx) assert result.mode == RagRuntimeMode.REACT def test_update_config(self, router): """[AC-AISVC-RES-15] Should update configuration.""" new_config = RoutingConfig( rag_runtime_mode=RagRuntimeMode.REACT, react_max_steps=7, ) router.update_config(new_config) assert router.config.rag_runtime_mode == RagRuntimeMode.REACT assert router.config.react_max_steps == 7 @pytest.mark.asyncio async def test_execute_direct(self, router): """[AC-AISVC-RES-09] Should execute direct retrieval.""" ctx = StrategyContext(tenant_id="tenant_a", query="Test query") mock_result = MagicMock() mock_result.hits = [] with patch.object( router._direct_executor, "execute", new_callable=AsyncMock ) as mock_execute: mock_execute.return_value = mock_result result = await router.execute_direct(ctx) assert result == mock_result @pytest.mark.asyncio async def test_execute_with_fallback_no_fallback(self, router): """[AC-AISVC-RES-14] Should not fallback when confidence is high.""" router._config.rag_runtime_mode = RagRuntimeMode.DIRECT router._config.direct_fallback_on_low_confidence = True router._config.direct_fallback_confidence_threshold = 0.4 ctx = StrategyContext(tenant_id="tenant_a", query="Test query") mock_result = MagicMock() mock_hit = MagicMock() mock_hit.score = 0.8 mock_result.hits = [mock_hit] with patch.object( router._direct_executor, "execute", new_callable=AsyncMock ) as mock_execute: mock_execute.return_value = mock_result result, answer, route_result = await router.execute_with_fallback(ctx) assert result == mock_result assert answer is None assert route_result.mode == RagRuntimeMode.DIRECT assert route_result.should_fallback_to_react is False @pytest.mark.asyncio async def test_execute_with_fallback_triggered(self, router): """[AC-AISVC-RES-14] Should fallback to react when confidence is low.""" router._config.rag_runtime_mode = RagRuntimeMode.DIRECT router._config.direct_fallback_on_low_confidence = True router._config.direct_fallback_confidence_threshold = 0.4 ctx = StrategyContext(tenant_id="tenant_a", query="Test query") mock_result = MagicMock() mock_hit = MagicMock() mock_hit.score = 0.2 mock_result.hits = [mock_hit] with patch.object( router._direct_executor, "execute", new_callable=AsyncMock ) as mock_direct: mock_direct.return_value = mock_result with patch.object( router._react_executor, "execute", new_callable=AsyncMock ) as mock_react: mock_react.return_value = ("Final answer", None, {}) result, answer, route_result = await router.execute_with_fallback(ctx) assert result is None assert answer == "Final answer" assert route_result.mode == RagRuntimeMode.REACT assert route_result.should_fallback_to_react is True assert route_result.fallback_reason == "low_confidence" class TestSingletonInstances: """Tests for singleton instance getters.""" def test_get_mode_router_singleton(self): """Should return same router instance.""" reset_mode_router() router1 = get_mode_router() router2 = get_mode_router() assert router1 is router2 def test_reset_mode_router(self): """Should create new instance after reset.""" router1 = get_mode_router() reset_mode_router() router2 = get_mode_router() assert router1 is not router2