""" Pytest configuration and fixtures for upgrade tests. """ import asyncio import logging import os from pathlib import Path import pytest from dotenv import load_dotenv # Configure logging for tests logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) # Reduce noise from httpx logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) def pytest_configure(config): """Load environment variables before running tests.""" # Look for .env in the workspace root env_file = Path(__file__).parent.parent.parent / ".env" if env_file.exists(): load_dotenv(env_file) _pg0_instance = None _pg0_url = None def _get_or_create_pg0(): """Get or create the shared pg0 instance for upgrade tests.""" global _pg0_instance, _pg0_url from hindsight_api.pg0 import EmbeddedPostgres if _pg0_instance is None: _pg0_instance = EmbeddedPostgres(name="hindsight-upgrade-test", port=5560) loop = asyncio.new_event_loop() try: _pg0_url = loop.run_until_complete(_pg0_instance.ensure_running()) finally: loop.close() return _pg0_url def _clean_database(db_url: str): """Drop all tables in the database to reset state for next test.""" from sqlalchemy import create_engine, text engine = create_engine(db_url) with engine.connect() as conn: # Drop all tables in public schema (cascade to handle foreign keys) tables = conn.execute( text(""" SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT LIKE 'pg_%' """) ).fetchall() for table in tables: conn.execute(text(f'DROP TABLE IF EXISTS public."{table[0]}" CASCADE')) conn.commit() engine.dispose() @pytest.fixture(scope="function") def db_url(): """ Provide a PostgreSQL connection URL for upgrade tests. Uses pg0 (embedded PostgreSQL) for a clean, isolated test database. The database is cleaned between tests to ensure fresh state for migrations. """ url = _get_or_create_pg0() # Clean database before each test _clean_database(url) yield url # No cleanup after - database is cleaned at start of next test @pytest.fixture(scope="module") def llm_config(): """ Provide LLM configuration from environment. Returns a dict with provider, api_key, and model. Note: Upgrade tests require a provider that is supported by old server versions. vertexai is only supported in newer versions, so upgrade tests are skipped when using vertexai provider without a fallback API key. """ provider = os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq") api_key = os.getenv("HINDSIGHT_API_LLM_API_KEY") or os.getenv("GROQ_API_KEY") model = os.getenv("HINDSIGHT_API_LLM_MODEL", "llama-3.3-70b-versatile") # Old server versions (e.g., v0.3.0) do not support vertexai provider. # Skip upgrade tests when using vertexai without a fallback traditional API key. providers_unsupported_by_old_versions = ("vertexai",) if provider in providers_unsupported_by_old_versions and not api_key: pytest.skip( f"Upgrade tests require a provider supported by old server versions. " f"Provider '{provider}' is not supported by older versions (e.g., v0.3.0). " f"Set HINDSIGHT_API_LLM_API_KEY to use a fallback provider." ) return { "provider": provider, "api_key": api_key, "model": model, } @pytest.fixture def unique_bank_id(): """Generate a unique bank ID for each test.""" import uuid return f"upgrade_test_{uuid.uuid4().hex[:8]}" @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """ Hook to dump server logs when a test fails. This captures all upgrade test server logs from /tmp/upgrade-test-*.log and displays them when a test fails, making CI debugging much easier. """ outcome = yield rep = outcome.get_result() # Only show logs for test failures (not setup/teardown failures) if rep.when == "call" and rep.failed: import glob log_files = sorted(glob.glob("/tmp/upgrade-test-*.log")) if log_files: print("\n" + "=" * 80) print("UPGRADE TEST SERVER LOGS (from failed test)") print("=" * 80) for log_file in log_files: print(f"\n--- {log_file} ---") try: with open(log_file) as f: content = f.read() # Limit to last 1000 lines to avoid overwhelming output lines = content.splitlines() if len(lines) > 1000: print(f"(showing last 1000 of {len(lines)} lines)") print("\n".join(lines[-1000:])) else: print(content) except Exception as e: print(f"Error reading log: {e}") print("=" * 80)