여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - 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>
95 lines
4.4 KiB
Python
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
|