o2o-site-AEO/solution/backend/tests/test_faq_fill.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

95 lines
4.4 KiB
Python

"""FAQ 목표 수 채우기 — ★ 공통 답은 문의 안내뿐이고, 이미 다룬 주제는 다시 넣지 않는다."""
import re
from common.enums import ErrorType, PlaceCategory
from common.faq_catalog import find_catalog
from services import faq_fill
from services.faq_fill import ExistingFaq
def _pension():
catalog = find_catalog(PlaceCategory.LODGING.value, "펜션")
assert catalog is not None
return catalog
def _ids(picks):
return {p.catalog_id for p in picks}
def test_pension_catalog_has_thirty_questions():
"""검증: 카탈로그 로드."""
catalog = _pension()
assert len(catalog.items) == 30
assert len({item.id for item in catalog.items}) == 30
def test_catalog_scope():
"""검증: 호텔·카페에는 펜션 질문을 붙이지 않는다."""
assert find_catalog(PlaceCategory.LODGING.value, "호텔") is None
assert find_catalog(PlaceCategory.CAFE.value, None) is None
assert find_catalog(PlaceCategory.LODGING.value, None) is not None
def test_fills_only_the_shortfall():
"""검증: 기존 FAQ 5건(카탈로그와 무관한 질문)."""
catalog = _pension()
picks = faq_fill.pick_fill_faqs(catalog, [ExistingFaq(f"기타 질문 {i}") for i in range(5)], set())
assert len(picks) == 15 and len(_ids(picks)) == 15
assert faq_fill.pick_fill_faqs(catalog, [ExistingFaq(f"질문 {i}") for i in range(20)], set()) == []
def test_skips_topics_answerable_by_facts():
"""검증: 체크인·반려동물 fact 가 있다(객실 fact 는 'A동:max_capacity' 로 적힌다)."""
picks = faq_fill.pick_fill_faqs(_pension(), [], {"check_in_time", "pet_allowed", "A동:max_capacity"}, target=30)
assert not _ids(picks) & {"check_in", "pet", "capacity"}
def test_skips_topics_already_asked_by_keyword():
"""검증: 사장님이 근거 key 없이 쓴 FAQ 가 있다."""
existing = [ExistingFaq("반려견 데려가도 되나요?"), ExistingFaq("주차 되나요?")]
picks = faq_fill.pick_fill_faqs(_pension(), existing, set(), target=30)
assert not _ids(picks) & {"pet", "parking"}
def test_skips_topics_covered_by_source_fact_keys():
"""검증: LLM FAQ 의 질문에는 키워드가 없지만 근거 key 가 [wifi, A동:max_capacity] 다."""
existing = [ExistingFaq("편의 안내가 궁금해요", ["wifi", "A동:max_capacity"])]
picks = faq_fill.pick_fill_faqs(_pension(), existing, set(), target=30)
assert not _ids(picks) & {"wifi", "capacity"}
def test_answers_are_inquiry_only():
"""검증: 공통 답 문구."""
catalog = _pension()
for pick in faq_fill.pick_fill_faqs(catalog, [], set(), target=30):
assert not re.search(r"\d", pick.answer), pick.answer
assert "숙소" in pick.answer
for pick in faq_fill.pick_fill_faqs(catalog, [], set(), phone="010-1234-5678", target=30):
assert "전화(010-1234-5678)로" in pick.answer
assert not re.search(r"\d", pick.answer.replace("010-1234-5678", "")), pick.answer
def test_answer_particle_and_rotation():
"""검증: 주제 뒤 조사(은/는)와 문구 돌려쓰기."""
catalog = _pension()
assert faq_fill.fallback_answer(catalog, 0, "보증금 여부", None).startswith("보증금 여부는 ")
assert faq_fill.fallback_answer(catalog, 0, "주변 편의시설", None).startswith("주변 편의시설은 ")
assert len({faq_fill.fallback_answer(catalog, i, "주제", None) for i in range(3)}) == 3
def test_suggested_questions_are_the_answerable_ones():
"""검증: 프롬프트에 실을 질문 = fact 로 답할 수 있는 카탈로그 질문."""
questions = faq_fill.suggested_questions(_pension(), {"parking", "check_out_time"})
assert questions == ["체크아웃은 몇 시까지인가요?", "주차할 수 있나요? 몇 대까지 가능한가요?"]
async def test_template_source_cannot_write_fact(auth_headers, client):
"""검증: fact 를 TEMPLATE 출처로 쓴다."""
h = await auth_headers("u1")
pid = (await client.post("/v1/place", headers=h, json={"name": "틀펜션", "category": 1})).json()["place"]["place_id"]
await client.post(f"/v1/place/{pid}/verify", headers=h, json={"source": 2, "road_address": "틀주소"})
r = await client.post(f"/v1/place/{pid}/fact", headers=h, json={
"key": "check_in_time", "value": "15:00", "source_type": 5, "source_url": "template:test"})
assert r.json()["result"]["code"] == ErrorType.INVALID_REQUEST_DATA.value