152 lines
4.4 KiB
Python
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
|