"""Tests for app.intent module.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock import pytest from app.intent import ( AMBIGUITY_THRESHOLD, ClassificationResult, IntentTarget, LLMIntentClassifier, _build_agent_list, ) from app.registry import AgentConfig def _make_agent(name: str, desc: str = "test", perm: str = "read") -> AgentConfig: return AgentConfig( name=name, description=desc, permission=perm, tools=["fallback_respond"], ) @pytest.mark.unit class TestIntentModels: def test_intent_target_frozen(self) -> None: target = IntentTarget(agent_name="order_lookup", confidence=0.9, reasoning="order query") with pytest.raises(Exception): target.agent_name = "other" # type: ignore[misc] def test_classification_result_frozen(self) -> None: result = ClassificationResult( intents=(IntentTarget(agent_name="a", confidence=0.9, reasoning="r"),), ) assert not result.is_ambiguous assert result.clarification_question is None def test_classification_result_ambiguous(self) -> None: result = ClassificationResult( intents=(), is_ambiguous=True, clarification_question="What do you mean?", ) assert result.is_ambiguous def test_multi_intent(self) -> None: result = ClassificationResult( intents=( IntentTarget(agent_name="order_actions", confidence=0.85, reasoning="cancel"), IntentTarget(agent_name="discount", confidence=0.8, reasoning="discount"), ), ) assert len(result.intents) == 2 @pytest.mark.unit class TestBuildAgentList: def test_formats_agents(self) -> None: agents = ( _make_agent("order_lookup", "Looks up orders", "read"), _make_agent("order_actions", "Modifies orders", "write"), ) text = _build_agent_list(agents) assert "order_lookup" in text assert "order_actions" in text assert "read" in text assert "write" in text @pytest.mark.unit class TestLLMIntentClassifier: @pytest.mark.asyncio async def test_single_intent_classification(self) -> None: expected = ClassificationResult( intents=(IntentTarget(agent_name="order_lookup", confidence=0.95, reasoning="query"),), ) mock_structured = MagicMock() mock_structured.ainvoke = AsyncMock(return_value=expected) mock_llm = MagicMock() mock_llm.with_structured_output = MagicMock(return_value=mock_structured) classifier = LLMIntentClassifier(mock_llm) agents = (_make_agent("order_lookup"), _make_agent("fallback")) result = await classifier.classify("What is order 1042 status?", agents) assert len(result.intents) == 1 assert result.intents[0].agent_name == "order_lookup" assert not result.is_ambiguous @pytest.mark.asyncio async def test_multi_intent_classification(self) -> None: expected = ClassificationResult( intents=( IntentTarget(agent_name="order_actions", confidence=0.9, reasoning="cancel"), IntentTarget(agent_name="discount", confidence=0.85, reasoning="discount"), ), ) mock_structured = MagicMock() mock_structured.ainvoke = AsyncMock(return_value=expected) mock_llm = MagicMock() mock_llm.with_structured_output = MagicMock(return_value=mock_structured) classifier = LLMIntentClassifier(mock_llm) agents = (_make_agent("order_actions"), _make_agent("discount"), _make_agent("fallback")) result = await classifier.classify("Cancel order 1042 and give me 10% off", agents) assert len(result.intents) == 2 assert not result.is_ambiguous @pytest.mark.asyncio async def test_ambiguous_classification(self) -> None: expected = ClassificationResult( intents=(IntentTarget(agent_name="fallback", confidence=0.3, reasoning="unclear"),), is_ambiguous=False, ) mock_structured = MagicMock() mock_structured.ainvoke = AsyncMock(return_value=expected) mock_llm = MagicMock() mock_llm.with_structured_output = MagicMock(return_value=mock_structured) classifier = LLMIntentClassifier(mock_llm) agents = (_make_agent("order_lookup"), _make_agent("fallback")) result = await classifier.classify("hmm", agents) # Low confidence triggers ambiguity assert result.is_ambiguous assert result.clarification_question is not None @pytest.mark.asyncio async def test_llm_error_returns_ambiguous(self) -> None: mock_structured = MagicMock() mock_structured.ainvoke = AsyncMock(side_effect=RuntimeError("LLM error")) mock_llm = MagicMock() mock_llm.with_structured_output = MagicMock(return_value=mock_structured) classifier = LLMIntentClassifier(mock_llm) agents = (_make_agent("fallback"),) result = await classifier.classify("test", agents) assert result.is_ambiguous assert result.clarification_question is not None @pytest.mark.asyncio async def test_non_result_type_returns_ambiguous(self) -> None: mock_structured = MagicMock() mock_structured.ainvoke = AsyncMock(return_value="not a ClassificationResult") mock_llm = MagicMock() mock_llm.with_structured_output = MagicMock(return_value=mock_structured) classifier = LLMIntentClassifier(mock_llm) agents = (_make_agent("fallback"),) result = await classifier.classify("test", agents) assert result.is_ambiguous @pytest.mark.asyncio async def test_high_confidence_not_ambiguous(self) -> None: expected = ClassificationResult( intents=( IntentTarget( agent_name="order_lookup", confidence=AMBIGUITY_THRESHOLD + 0.1, reasoning="clear", ), ), ) mock_structured = MagicMock() mock_structured.ainvoke = AsyncMock(return_value=expected) mock_llm = MagicMock() mock_llm.with_structured_output = MagicMock(return_value=mock_structured) classifier = LLMIntentClassifier(mock_llm) agents = (_make_agent("order_lookup"),) result = await classifier.classify("order status 1042", agents) assert not result.is_ambiguous