o2o-negosium-original/agent/tests/test_negotiation_step_preview.py
hbyang be1b4b968f [fix] agent: 보상함수를 q-table 상세 설명 v4 명세에 정합화
- 동적 가중치 W: 라운드 감쇠 → 상태 5차원 가중합 clip(Σwᵢ·Sᵢ, 0.2, 0.8) (식 12~13, 기존 w1~w5 연결)
- 종료보상에 (1−W) 적용: R = W×R_price + (1−W)×R_end − λ×round (식 8)
- R_price 3단계: P<anchor 시 1+β·(anchor−P)/anchor 초과달성 보너스 추가 (식 9~11, beta 의미 재정의)
- price zone 경계는 명세(T)와 달리 anchor 유지(우선협상 규칙이 실제 의사결정 경계) — 사유 docstring 명시
- state_calculator/config 의 낡은 반대 컨벤션(anchor≥target) 주석 정정
- RewardCalculator(RewardConfig, StateConfig) 시그니처 변경 + 호출부 5곳 갱신, 테스트 기대값 정정 (76/76 PASS)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:10:44 +09:00

62 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""/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": "ktcommerce"}, 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 에 따라 다른 상태/카드로 갈린다
assert dk["card_id"].startswith("NGC-A")
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": "ktcommerce"}, 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": "ktcommerce"}, json=body)
assert r.json()["result"]["desc"] == "INVALID_REQUEST_DATA"