Files
Gov-chat-bot/backend/tests/test_tier_a_routing.py
2026-03-26 12:49:43 +09:00

152 lines
4.4 KiB
Python

"""
Tier A 라우팅 통합 테스트.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
from app.services.routing import ResponseRouter, RoutingResult
from app.providers.base import SearchResult
def make_router(embedding=None, vectordb=None) -> ResponseRouter:
providers = {}
if embedding:
providers["embedding"] = embedding
if vectordb:
providers["vectordb"] = vectordb
return ResponseRouter(
tenant_config={
"phone_number": "031-860-2000",
"fallback_dept": "민원과",
"tenant_name": "동두천시",
},
providers=providers,
)
@pytest.mark.asyncio
async def test_tier_a_returns_when_faq_found():
"""벡터DB에서 FAQ 매칭 → Tier A 반환."""
faq_id = str(uuid4())
faq = MagicMock()
faq.id = faq_id
faq.answer = "여권은 민원과에서 발급합니다."
faq.question = "여권 발급 방법"
faq.is_active = True
faq.hit_count = 0
faq.updated_at = None
embedding = AsyncMock()
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
vectordb = AsyncMock()
vectordb.search = AsyncMock(return_value=[
SearchResult(text="여권 발급 방법\n여권은 민원과에서 발급합니다.",
doc_id=f"{faq_id}_0", score=0.92, metadata={"faq_id": faq_id})
])
db = AsyncMock()
db.execute = AsyncMock()
scalar = MagicMock()
scalar.scalar_one_or_none = MagicMock(return_value=faq)
db.execute.return_value = scalar
db.get = AsyncMock(return_value=faq)
db.commit = AsyncMock()
router = make_router(embedding=embedding, vectordb=vectordb)
result = await router.route("tenant-1", "여권 발급 방법", "user-1", db=db)
assert result.tier == "A"
assert result.source == "faq"
assert result.answer == faq.answer
@pytest.mark.asyncio
async def test_tier_a_skipped_when_no_match():
"""매칭 없으면 Tier D 반환."""
embedding = AsyncMock()
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
vectordb = AsyncMock()
vectordb.search = AsyncMock(return_value=[]) # 빈 결과
db = AsyncMock()
router = make_router(embedding=embedding, vectordb=vectordb)
result = await router.route("tenant-1", "모르는 질문", "user-1", db=db)
assert result.tier == "D"
assert result.source == "fallback"
@pytest.mark.asyncio
async def test_routing_result_has_faq_id():
"""Tier A 결과에 faq_id가 포함된다."""
faq_id = str(uuid4())
faq = MagicMock()
faq.id = faq_id
faq.answer = "답변"
faq.question = "질문"
faq.is_active = True
faq.hit_count = 0
faq.updated_at = None
embedding = AsyncMock()
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
vectordb = AsyncMock()
vectordb.search = AsyncMock(return_value=[
SearchResult(text="Q\nA", doc_id=f"{faq_id}_0", score=0.90,
metadata={"faq_id": faq_id})
])
db = AsyncMock()
db.execute = AsyncMock()
scalar = MagicMock()
scalar.scalar_one_or_none = MagicMock(return_value=faq)
db.execute.return_value = scalar
db.get = AsyncMock(return_value=faq)
db.commit = AsyncMock()
router = make_router(embedding=embedding, vectordb=vectordb)
result = await router.route("tenant-1", "질문", "user-1", db=db)
assert result.faq_id == faq_id
@pytest.mark.asyncio
async def test_routing_result_tier_d_has_fallback_info():
"""Tier D 결과에 담당부서·전화번호가 포함된다."""
router = make_router()
result = await router.route("tenant-1", "알 수 없는 질문", "user-1", db=None)
assert result.tier == "D"
assert "031-860-2000" in result.answer
assert "민원과" in result.answer
@pytest.mark.asyncio
async def test_timeout_returns_tier_d_is_timeout():
"""타임아웃 발생 시 Tier D + is_timeout=True."""
import asyncio
async def slow_embed(texts):
await asyncio.sleep(10) # 타임아웃보다 긴 대기
return [[0.0] * 768]
embedding = AsyncMock()
embedding.embed = AsyncMock(side_effect=slow_embed)
vectordb = AsyncMock()
# 타임아웃을 매우 짧게 설정
router = make_router(embedding=embedding, vectordb=vectordb)
router.TIMEOUT_MS = 50 # 50ms
db = AsyncMock()
result = await router.route("tenant-1", "질문", "user-1", db=db)
assert result.tier == "D"
assert result.is_timeout is True