106 lines
4.5 KiB
Python
106 lines
4.5 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("ktcommerce")
|
||
|
||
# 우리 플랫폼 중립 기본값 (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("ktcommerce")
|
||
assert cfg.state.state_space_size == 162 # 3×3×3×3×2 (차원 구성은 기능적 설계)
|
||
assert cfg.action_mapping.action_space_size == 9
|
||
# 합성 데모 카드 코드 (우리 스킴)
|
||
assert cfg.action_mapping.action_to_card["0"] == "NGC-A001"
|
||
assert cfg.action_mapping.action_to_card["8"] == "NGC-A009"
|
||
|
||
|
||
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 == 9
|
||
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"
|
||
assert cfg.action_mapping.action_space_size == 0
|
||
|
||
|
||
def test_is_registered():
|
||
loader = _loader()
|
||
assert loader.is_registered("ktcommerce") is True
|
||
assert loader.is_registered("imarketkorea") is True
|
||
assert loader.is_registered("nonexistent_tenant") is False
|
||
|
||
|
||
def test_no_proprietary_card_codes_or_labels_in_repo():
|
||
"""클린룸 가드: 독점 카드 코드/ verbatim 라벨이 로드된 config 에 존재하지 않는다."""
|
||
for tid in ("_base", "ktcommerce", "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
|