o2o-negosium-original/agent/tests/test_p4_registry_middleware.py

113 lines
4.1 KiB
Python

"""P4 검증 (계획서 P4 _검증_).
1. 두 테넌트가 서로 다른 엔진/카드매핑(q_table 차원·action) 사용.
2. 엔진 지연생성 + 캐시(동일 테넌트는 동일 인스턴스).
3. 동시 첫 요청에서 lock 으로 1회만 조립 (동시성).
4. 미등록 테넌트 get_engine → KeyError.
5. 미들웨어: 헤더 누락 400, 미등록 404, 등록 테넌트는 통과.
6. episode 상태 외부화: EpisodeState 는 요청 스코프(엔진에 없음).
"""
import asyncio
import os
import pytest
from negotiation.policies.base import EpisodeState
from tenancy.config_loader import TenantConfigLoader
from tenancy.registry import EngineFactory, TenantEngine, TenantEngineRegistry
_TENANTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tenants")
def _registry() -> TenantEngineRegistry:
return TenantEngineRegistry(loader=TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0))
@pytest.mark.asyncio
async def test_two_tenants_distinct_engines():
reg = _registry()
e1 = await reg.get_engine("ktcommerce")
e2 = await reg.get_engine("imarketkorea")
assert e1 is not e2
assert e1.tenant_id == "ktcommerce" and e2.tenant_id == "imarketkorea"
# 서로 다른 카드매핑 (다른 카드셋)
assert e1.mapper.get_card_id(0) == "NGC-A001"
assert e2.mapper.get_card_id(0) == "NGC-B001"
# 차원
assert e1.state_space_size == 162 and e1.action_space_size == 9
@pytest.mark.asyncio
async def test_engine_cached():
reg = _registry()
a = await reg.get_engine("ktcommerce")
b = await reg.get_engine("ktcommerce")
assert a is b # 캐시 — 동일 인스턴스
@pytest.mark.asyncio
async def test_concurrent_first_build_once():
# 조립 횟수 카운트용 팩토리
builds = {"n": 0}
class CountingFactory(EngineFactory):
@staticmethod
def build(config):
builds["n"] += 1
return EngineFactory.build(config)
reg = TenantEngineRegistry(loader=TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0), factory=CountingFactory)
results = await asyncio.gather(*[reg.get_engine("ktcommerce") for _ in range(12)])
# 모두 같은 인스턴스 + 1회만 조립
assert all(r is results[0] for r in results)
assert builds["n"] == 1
@pytest.mark.asyncio
async def test_unregistered_raises():
reg = _registry()
with pytest.raises(KeyError):
await reg.get_engine("nonexistent_tenant")
assert reg.is_registered("ktcommerce") is True
assert reg.is_registered("nonexistent_tenant") is False
@pytest.mark.asyncio
async def test_reload_rebuilds_only_that_tenant():
reg = _registry()
a = await reg.get_engine("ktcommerce")
b = await reg.get_engine("imarketkorea")
reloaded = await reg.reload("ktcommerce")
assert reloaded is not a # 재조립됨
assert await reg.get_engine("imarketkorea") is b # 타테넌트는 그대로
def test_episode_state_is_request_scoped():
# 엔진은 episode 상태를 갖지 않는다 — EpisodeState 는 독립 객체.
es1 = EpisodeState()
es2 = EpisodeState()
es1.mark_used(3)
assert es1.used_action_ids == {3}
assert es2.used_action_ids == set() # 서로 오염 없음
assert not hasattr(TenantEngine, "used_action_ids")
@pytest.mark.asyncio
async def test_middleware_header_missing_unregistered_registered(client):
# 헤더 누락 → 400
r = await client.get("/v1/protected")
assert r.status_code == 400
assert r.json()["result"]["desc"] == "TENANT_HEADER_MISSING"
# 미등록 → 404 (TENANT_NOT_REGISTERED)
r = await client.get("/v1/protected", headers={"X-Tenant-ID": "nonexistent_tenant"})
assert r.status_code == 404
assert r.json()["result"]["desc"] == "TENANT_NOT_REGISTERED"
# 등록 테넌트 → 미들웨어 통과 (라우트 미존재라 404지만 TENANT_NOT_REGISTERED 아님)
r = await client.get("/v1/protected", headers={"X-Tenant-ID": "ktcommerce"})
assert r.status_code == 404
body = r.json()
assert body.get("result", {}).get("desc") != "TENANT_NOT_REGISTERED"