카드 재설계("멘트 카드 → 전술 카드"):
- 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>
79 lines
3.3 KiB
Python
79 lines
3.3 KiB
Python
# 테스트도 APP_ENV=local 로 실행한다 (config.local.toml 사용).
|
|
# config.server_configs 가 import 되는 순간 config.<APP_ENV>.toml 을 읽으므로 가장 먼저 설정.
|
|
import os
|
|
|
|
os.environ.setdefault("APP_ENV", "local")
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_real_llm(monkeypatch):
|
|
"""실 LLM 호출 차단 — imarketkorea 가 llm.enabled=true 라 로컬에 실키가 있으면
|
|
챗 플로우가 자연화/NLU(과금·비결정 응답)를 시도한다. LLM 검증 테스트는
|
|
available 을 테스트 안에서 직접 덮어써 이 가드를 우회한다."""
|
|
from negotiation.chat.service.input_interpreter import InputInterpreter
|
|
from negotiation.chat.service.script_naturalizer import ScriptNaturalizer
|
|
|
|
monkeypatch.setattr(ScriptNaturalizer, "available", staticmethod(lambda: False))
|
|
monkeypatch.setattr(InputInterpreter, "available", staticmethod(lambda: False))
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="session", autouse=True)
|
|
async def _dispose_app_engines():
|
|
"""테스트 세션 종료 시 앱 싱글톤 엔진 정리 ('Event loop is closed' 경고 제거)."""
|
|
yield
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
|
|
await DB_SESSION_MNG.dispose_all()
|
|
|
|
|
|
def _write_url(cfg) -> str:
|
|
pw = f":{cfg.write_pw}" if cfg.write_pw else ""
|
|
return f"postgresql+asyncpg://{cfg.write_id}{pw}@{cfg.write_host}:{cfg.write_port}/{cfg.name}"
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def db_engine():
|
|
"""learning 스키마 테이블을 보장하고, 매 테스트 시작 시 비워 격리한다.
|
|
|
|
DB 미가용(로컬 postgres 없음) 시 해당 테스트를 skip 한다.
|
|
앱(DB_SESSION_MNG)은 같은 config 로 같은 DB 에 접속하므로 스키마를 공유한다.
|
|
"""
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
from common.database.model.models import MAIN_BASE, LEARNING_SCHEMA
|
|
from config.server_configs import main_db_config
|
|
|
|
engine = create_async_engine(_write_url(main_db_config))
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text('CREATE EXTENSION IF NOT EXISTS pgcrypto'))
|
|
await conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS {LEARNING_SCHEMA}"))
|
|
await conn.run_sync(MAIN_BASE.metadata.create_all) # 이미 있으면 skip
|
|
for tbl in ("experience_logs", "q_values", "visit_counts", "q_table_versions", "tenant_action_cards", "chat_sessions"):
|
|
await conn.execute(text(f"TRUNCATE TABLE {LEARNING_SCHEMA}.{tbl} RESTART IDENTITY CASCADE"))
|
|
except Exception as ex:
|
|
await engine.dispose()
|
|
pytest.skip(f"DB 미가용 — P3 DB 테스트 skip: {type(ex).__name__}: {str(ex)[:80]}")
|
|
yield engine
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client():
|
|
"""앱을 실제 네트워크 없이 호출하는 httpx 클라이언트 (ASGITransport).
|
|
|
|
P0 스모크는 DB 테이블을 요구하지 않는 경로(healthz/health)만 검증한다.
|
|
learning 스키마 테이블·DB 의존 테스트는 P3 이후 db_engine 픽스처를 추가해 다룬다.
|
|
"""
|
|
from router.router import app
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
yield ac
|