o2o-negosium-original/agent/tests/test_p1_tenant_config.py
hbyang 18020a9e07 [feat] 카드 카탈로그 DB 정본화 — config 결합 제거 + 학습 보존 자동반영
카드 카탈로그(negodata)가 Q-table action space 를 정의하는 정본이 되고, 카드 변경이
config 수정·학습 손실 없이 agent 에 자동 반영되는 고리를 완성.

- action space 정리: 카탈로그 전체(NGC-001~011, 11장) 고정, 견적별 선택은 축소가 아니라
  available_mask(_selection_mask) 로 처리 — action_id↔카드 대응을 견적마다 일정하게 유지해
  Q-table 학습 일관성 보장. 구 인덱스 방식(selected[action_id]) 폐기.
- ① 카탈로그 DB 정본화: action_mapping.type=db 면 registry 가 card.nego_cards(user_id NULL,
  number 순) 조회로 action_to_card 동적 구성(파일은 폴백). port/adapter(card_catalog_*).
  _base=type:db. → negodata 카드 추가/삭제 시 config 수정 불필요.
- ② 차원 변경 학습 보존 마이그레이션: migrate_active_version_dim — 겹치는 셀 복사
  (append/truncate 안전) + 새 카드 fresh. model_store.load 가 차원 불일치 시 호출.
- ③ reload 엔드포인트: /v1/catalog-refresh(테넌트) · /v1/catalog-refresh-all(전역, 화이트리스트).
- ④ 브랜드: company_profile_repo — 자동 온보딩 고객사(company_id UUID)는
  company.companies.name 으로 {company_name} 채움. 데모 테넌트는 파일 유지.
- 크로스서비스: negodata card_service 가 공용 nego 카드 변경 시 agent_notify 로 전역 리로드 알림
  (best-effort, is_test skip). config 에 agent_base_url.
- 하니스 episodes 400→600(action 11 수렴). 테스트 갱신·추가로 agent 98/98.

알려진 갭(후속): per-company 카탈로그 스코프(회사 카드도 action space 포함), 카탈로그 중간
삭제 시 카드번호 기반 마이그레이션.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:56:28 +09:00

109 lines
4.8 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.

"""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 == 11
# 합성 데모 카드 코드 (우리 스킴)
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 == 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("ktcommerce") is True
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", "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