카드 재설계("멘트 카드 → 전술 카드"):
- 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>
108 lines
4.8 KiB
Python
108 lines
4.8 KiB
Python
"""P1 검증 (계획서 P1, CLEANROOM.md 반영).
|
||
|
||
검증 기준 변경: "Chat_server 하드코딩과 1:1 일치"(독점값 복제)를 폐기하고,
|
||
"우리 플랫폼 중립 기본값이 정확히 로드 + deep-merge + 차원 산출이 동작"으로 대체한다.
|
||
|
||
1. 데모 테넌트 config 가 플랫폼 중립 기본값으로 로드된다(합성 카드 코드/중립 라벨).
|
||
2. _base deep-merge 단위테스트 (상속 + 부분 오버라이드).
|
||
3. state_space_size 자동 산출 (3×3×3×3×2 = 162), action_space_size = 9.
|
||
4. 두 번째 테넌트 오버라이드가 base 위에 정확히 병합 + 차원 동일(warm-start 호환).
|
||
5. 독점 카드 코드(NC26-*)·verbatim 라벨이 레포 config 에 없다(클린룸 가드).
|
||
"""
|
||
|
||
import os
|
||
|
||
from tenancy.config_loader import TenantConfigLoader, _deep_merge
|
||
|
||
_TENANTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tenants")
|
||
|
||
|
||
def _loader() -> TenantConfigLoader:
|
||
return TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0)
|
||
|
||
|
||
def test_platform_neutral_defaults_load():
|
||
cfg = _loader().load("_base")
|
||
|
||
# 우리 플랫폼 중립 기본값 (CLEANROOM.md)
|
||
assert cfg.state.revenue.thresholds == [10_000_000, 50_000_000]
|
||
assert cfg.state.revenue.weights == [0.3, 0.6, 1.0]
|
||
assert cfg.state.revenue.descriptions == ["low", "mid", "high"]
|
||
assert cfg.state.distribution.code_map == {"A": 0, "B": 1, "C": 2}
|
||
assert cfg.state.partner.weights == [0.5, 1.0, 0.3]
|
||
assert cfg.state.acceptance.thresholds == [0.03, 0.09]
|
||
assert cfg.state.price_zone.weights == [1.0, 0.5]
|
||
|
||
# reward: 균등 가중치 중립 기본값
|
||
assert cfg.reward.beta == 0.2
|
||
assert cfg.reward.success_reward == 1.0
|
||
assert cfg.reward.failure_penalty == -0.5
|
||
assert (cfg.reward.w1, cfg.reward.w2, cfg.reward.w3, cfg.reward.w4, cfg.reward.w5) == (0.2, 0.2, 0.2, 0.2, 0.2)
|
||
|
||
# policy: 표준 UCB 기본값
|
||
assert cfg.policy.type == "ucb"
|
||
assert cfg.policy.learning_rate == 0.1
|
||
assert cfg.policy.gamma == 0.95
|
||
assert abs(cfg.policy.params["exploration_constant"] - 2 ** 0.5) < 1e-12
|
||
|
||
|
||
def test_state_space_and_action_space_size():
|
||
cfg = _loader().load("_base")
|
||
assert cfg.state.state_space_size == 162 # 3×3×3×3×2 (차원 구성은 기능적 설계)
|
||
assert cfg.action_mapping.action_space_size == 11
|
||
# 공용 카드 코드 (파일 폴백 스냅샷 — 정본은 DB 카탈로그)
|
||
assert cfg.action_mapping.action_to_card["0"] == "NGC-001"
|
||
assert cfg.action_mapping.action_to_card["8"] == "NGC-009"
|
||
|
||
|
||
def test_base_deep_merge_unit():
|
||
base = {"a": 1, "nested": {"x": 1, "y": 2}, "list": [1, 2]}
|
||
override = {"b": 2, "nested": {"y": 20, "z": 30}, "list": [9]}
|
||
merged = _deep_merge(base, override)
|
||
assert merged["a"] == 1
|
||
assert merged["b"] == 2
|
||
assert merged["nested"] == {"x": 1, "y": 20, "z": 30} # dict 키 단위 병합
|
||
assert merged["list"] == [9] # 리스트는 통째 교체
|
||
|
||
|
||
def test_second_tenant_overrides_merged_on_base():
|
||
cfg = _loader().load("imarketkorea")
|
||
# 오버라이드된 값
|
||
assert cfg.state.revenue.thresholds == [30_000_000, 100_000_000]
|
||
assert cfg.reward.failure_penalty == -0.7
|
||
assert cfg.reward.beta == 0.25
|
||
# 오버라이드 안 한 값은 base 상속
|
||
assert cfg.state.distribution.code_map == {"A": 0, "B": 1, "C": 2}
|
||
assert cfg.reward.success_reward == 1.0
|
||
assert cfg.policy.type == "ucb"
|
||
# 차원은 데모 테넌트 A 와 동일(162) → base warm-start 호환(P5)
|
||
assert cfg.state.state_space_size == 162
|
||
assert cfg.action_mapping.action_space_size == 11
|
||
assert cfg.action_mapping.action_to_card["0"] == "NGC-B001"
|
||
|
||
|
||
def test_base_self_does_not_inherit():
|
||
cfg = _loader().load("_base")
|
||
assert cfg.tenant_id == "_base"
|
||
# _base 는 기본 11카드(자동 온보딩 테넌트가 물려받음, base 정책 162×11 정합)
|
||
assert cfg.action_mapping.action_space_size == 11
|
||
|
||
|
||
def test_is_registered():
|
||
loader = _loader()
|
||
assert loader.is_registered("imarketkorea") is True
|
||
# 미등록 company_id(uuid 등)는 _base 자동 온보딩 대상이라 '등록됨'으로 본다. 빈 키만 미등록.
|
||
assert loader.is_registered("00000000-0000-0000-0000-000000000001") is True
|
||
assert loader.is_registered("") is False
|
||
|
||
|
||
def test_no_proprietary_card_codes_or_labels_in_repo():
|
||
"""클린룸 가드: 독점 카드 코드/ verbatim 라벨이 로드된 config 에 존재하지 않는다."""
|
||
for tid in ("_base", "imarketkorea"):
|
||
cfg = _loader().load(tid)
|
||
cards = " ".join(cfg.action_mapping.action_to_card.values())
|
||
assert "NC26" not in cards # 참고 엔진의 고유 카드 코드
|
||
# verbatim 한글 라벨이 아닌 중립 라벨 사용
|
||
for d in cfg.state.revenue.descriptions:
|
||
assert "원" not in d
|