카드 재설계("멘트 카드 → 전술 카드"):
- tactics.py 신설: 카드번호→전술(카운터 산식) 레지스트리, min(counter,target) 클램프
- 카운터 수락=즉시 타결(pending_counter_price 일반화, 구 offer_1pct 흡수)
- 목표가 초과 타결 금지(성공스텝 진입 가드) + "카드 소진=실패" 폐지→종결 국면
- 선택형 와일드카드(WC-*) 발동 + card.wild_cards 멘트 DB 어댑터
LLM 계층:
- Phase 2 표현층 ScriptNaturalizer(카드 멘트 자연화, 마커·치환자·숫자 보존 검증)
- Phase 3 이해층 InputInterpreter(자유발화 NLU→기대입력, 한국어 가격 파서)
- OPENAI_API_KEY env override(server_configs) + 전역 자격증명 게이트
결정 스택(Phase 1):
- 협상 규칙 데이터화(negotiation.wildcard_*_ratio/max_counter_rounds)
- 선택카드 우선순위 prior(UCB 방문수 감쇠, Q-table 오염 없음)
버그픽스:
- 인하율 음수 표기 제거 + 인상/동일/인하 구분(discount_phrase)
- 자연화 강조마커 보존(볼드/색 소실 시 원본 폴백)
- 카드 시드 가격변수(prev_partner_price·target_mid_price·middle_price 등) 치환
정리:
- ktcommerce 테넌트 삭제 + 테스트 21파일 imarketkorea/_base 로 마이그레이션
- 실 LLM 호출 차단 conftest 가드
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
2.6 KiB
Python
62 lines
2.6 KiB
Python
"""/v1/negotiation/step (P2 루프 HTTP 프리뷰) 회귀 스모크.
|
||
|
||
서버 기동 후 실제로 칠 수 있는 유일한 업무 엔드포인트 — 테넌트 라우팅 + config 주입 + 응답 형태를 검증.
|
||
DB 로깅은 db_engine 픽스처 유무와 무관하게 log=false 로 끄고 검증(순수 응답 형태).
|
||
"""
|
||
|
||
import pytest
|
||
|
||
_BODY = {
|
||
"revenue_amount": 20_000_000,
|
||
"distribution_code": "A",
|
||
"partner_count": 1,
|
||
"acceptance_ratio": 0.11,
|
||
"input_price": 9950,
|
||
"anchor_price": 9900, # KT 앵커링가 (anchor < target)
|
||
"target_price": 10000, # KT 목표 매입가
|
||
"round_number": 3,
|
||
"outcome": "success",
|
||
"log": False,
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_step_requires_tenant_header(client):
|
||
r = await client.post("/v1/negotiation/step", json=_BODY)
|
||
assert r.status_code == 400
|
||
assert r.json()["result"]["desc"] == "TENANT_HEADER_MISSING"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_step_tenant_divergence(client):
|
||
rk = await client.post("/v1/negotiation/step", headers={"X-Tenant-ID": "_base"}, json=_BODY)
|
||
ri = await client.post("/v1/negotiation/step", headers={"X-Tenant-ID": "imarketkorea"}, json=_BODY)
|
||
assert rk.status_code == 200 and ri.status_code == 200
|
||
dk, di = rk.json(), ri.json()
|
||
# 같은 입력이 테넌트 config 에 따라 다른 상태/카드로 갈린다 (_base=공용 NGC-0xx, imk=NGC-Bxxx)
|
||
assert dk["card_id"].startswith("NGC-0")
|
||
assert di["card_id"].startswith("NGC-B")
|
||
assert dk["state_index"] != di["state_index"]
|
||
# 응답 형태
|
||
assert dk["result"]["success"] is True
|
||
# input 9950: price_reward=(10000-9950)/(10000-9900)=0.5,
|
||
# W=Σ0.2·Sᵢ=0.2×(0.6+0.3+0.5+1.0+0.5)=0.58 (revenue mid, dist A, single, accept high, zone1),
|
||
# penalty=0.02×3=0.06, end(success)=1.0 → 0.58×0.5 + 0.42×1.0 − 0.06 = 0.65
|
||
assert dk["reward"]["total"] == pytest.approx(0.65, abs=1e-6)
|
||
assert dk["logged"] is False # log=false
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_step_invalid_distribution_code_is_domain_error(client):
|
||
body = dict(_BODY, distribution_code="Z")
|
||
r = await client.post("/v1/negotiation/step", headers={"X-Tenant-ID": "imarketkorea"}, json=body)
|
||
assert r.status_code == 200 # HTTP 는 200, 결과코드로 에러 전달(backend 규약)
|
||
assert r.json()["result"]["desc"] == "NEGO_INVALID_STEP"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_step_invalid_outcome(client):
|
||
body = dict(_BODY, outcome="maybe")
|
||
r = await client.post("/v1/negotiation/step", headers={"X-Tenant-ID": "imarketkorea"}, json=body)
|
||
assert r.json()["result"]["desc"] == "INVALID_REQUEST_DATA"
|