# 테스트도 APP_ENV=local 로 실행한다 (config.local.toml 사용). # config.server_configs 가 import 되는 순간 config..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