Files
smart-support/backend/tests/unit/test_intent.py
Yaojia Wang 006b4ee5d7 fix: resolve ruff lint errors in Phase 2 code
- Move intent imports to TYPE_CHECKING block in graph.py (TC001)
- Rename test classes to CapWords convention (N801)
- Fix line length violations across test files (E501)
- Auto-fix import sorting (I001)
2026-03-30 21:44:47 +02:00

178 lines
6.4 KiB
Python

"""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