"""Pytest configuration and fixtures for the test suite.""" import asyncio from datetime import datetime, timezone from typing import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from sqlalchemy import event from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.pool import StaticPool from daemon_boyfriend.config import Settings from daemon_boyfriend.models.base import Base # --- Event Loop Fixture --- @pytest.fixture(scope="session") def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: """Create an event loop for the test session.""" loop = asyncio.new_event_loop() yield loop loop.close() # --- Database Fixtures --- @pytest_asyncio.fixture async def async_engine(): """Create an async SQLite engine for testing.""" engine = create_async_engine( "sqlite+aiosqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, echo=False, ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest_asyncio.fixture async def db_session(async_engine) -> AsyncGenerator[AsyncSession, None]: """Create a database session for testing.""" async_session_maker = async_sessionmaker( async_engine, class_=AsyncSession, expire_on_commit=False, ) async with async_session_maker() as session: yield session await session.rollback() # --- Mock Settings Fixture --- @pytest.fixture def mock_settings() -> Settings: """Create mock settings for testing.""" with patch.dict( "os.environ", { "DISCORD_TOKEN": "test_token", "AI_PROVIDER": "openai", "AI_MODEL": "gpt-4o-mini", "OPENAI_API_KEY": "test_openai_key", "ANTHROPIC_API_KEY": "test_anthropic_key", "GEMINI_API_KEY": "test_gemini_key", "OPENROUTER_API_KEY": "test_openrouter_key", "BOT_NAME": "TestBot", "BOT_PERSONALITY": "helpful and friendly", "DATABASE_URL": "", "LIVING_AI_ENABLED": "true", "MOOD_ENABLED": "true", "RELATIONSHIP_ENABLED": "true", }, ): return Settings() # --- Mock Discord Fixtures --- @pytest.fixture def mock_discord_user() -> MagicMock: """Create a mock Discord user.""" user = MagicMock() user.id = 123456789 user.name = "TestUser" user.display_name = "Test User" user.mention = "<@123456789>" user.bot = False return user @pytest.fixture def mock_discord_message(mock_discord_user) -> MagicMock: """Create a mock Discord message.""" message = MagicMock() message.author = mock_discord_user message.content = "Hello, bot!" message.channel = MagicMock() message.channel.id = 987654321 message.channel.send = AsyncMock() message.channel.typing = MagicMock(return_value=AsyncMock()) message.guild = MagicMock() message.guild.id = 111222333 message.guild.name = "Test Guild" message.id = 555666777 message.mentions = [] return message @pytest.fixture def mock_discord_bot() -> MagicMock: """Create a mock Discord bot.""" bot = MagicMock() bot.user = MagicMock() bot.user.id = 999888777 bot.user.name = "TestBot" bot.user.mentioned_in = MagicMock(return_value=True) return bot # --- Mock AI Provider Fixtures --- @pytest.fixture def mock_ai_response() -> MagicMock: """Create a mock AI response.""" from daemon_boyfriend.services.providers.base import AIResponse return AIResponse( content="This is a test response from the AI.", model="test-model", usage={"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30}, ) @pytest.fixture def mock_openai_client() -> MagicMock: """Create a mock OpenAI client.""" client = MagicMock() response = MagicMock() response.choices = [MagicMock()] response.choices[0].message.content = "Test OpenAI response" response.model = "gpt-4o-mini" response.usage = MagicMock() response.usage.prompt_tokens = 10 response.usage.completion_tokens = 20 response.usage.total_tokens = 30 client.chat.completions.create = AsyncMock(return_value=response) return client @pytest.fixture def mock_anthropic_client() -> MagicMock: """Create a mock Anthropic client.""" client = MagicMock() response = MagicMock() response.content = [MagicMock()] response.content[0].type = "text" response.content[0].text = "Test Anthropic response" response.model = "claude-sonnet-4-20250514" response.usage = MagicMock() response.usage.input_tokens = 10 response.usage.output_tokens = 20 client.messages.create = AsyncMock(return_value=response) return client @pytest.fixture def mock_gemini_client() -> MagicMock: """Create a mock Gemini client.""" client = MagicMock() response = MagicMock() response.text = "Test Gemini response" response.usage_metadata = MagicMock() response.usage_metadata.prompt_token_count = 10 response.usage_metadata.candidates_token_count = 20 response.usage_metadata.total_token_count = 30 client.aio.models.generate_content = AsyncMock(return_value=response) return client # --- Model Fixtures --- @pytest_asyncio.fixture async def sample_user(db_session: AsyncSession): """Create a sample user in the database.""" from daemon_boyfriend.models import User user = User( discord_id=123456789, discord_username="testuser", discord_display_name="Test User", ) db_session.add(user) await db_session.commit() await db_session.refresh(user) return user @pytest_asyncio.fixture async def sample_user_with_facts(db_session: AsyncSession, sample_user): """Create a sample user with facts.""" from daemon_boyfriend.models import UserFact facts = [ UserFact( user_id=sample_user.id, fact_type="hobby", fact_content="likes programming", confidence=1.0, source="explicit", ), UserFact( user_id=sample_user.id, fact_type="preference", fact_content="prefers dark mode", confidence=0.8, source="conversation", ), ] for fact in facts: db_session.add(fact) await db_session.commit() return sample_user @pytest_asyncio.fixture async def sample_conversation(db_session: AsyncSession, sample_user): """Create a sample conversation.""" from daemon_boyfriend.models import Conversation conversation = Conversation( user_id=sample_user.id, guild_id=111222333, channel_id=987654321, ) db_session.add(conversation) await db_session.commit() await db_session.refresh(conversation) return conversation @pytest_asyncio.fixture async def sample_bot_state(db_session: AsyncSession): """Create a sample bot state.""" from daemon_boyfriend.models import BotState bot_state = BotState( guild_id=111222333, mood_valence=0.5, mood_arousal=0.3, ) db_session.add(bot_state) await db_session.commit() await db_session.refresh(bot_state) return bot_state @pytest_asyncio.fixture async def sample_user_relationship(db_session: AsyncSession, sample_user): """Create a sample user relationship.""" from daemon_boyfriend.models import UserRelationship relationship = UserRelationship( user_id=sample_user.id, guild_id=111222333, relationship_score=50.0, total_interactions=10, positive_interactions=8, negative_interactions=1, ) db_session.add(relationship) await db_session.commit() await db_session.refresh(relationship) return relationship # --- Utility Fixtures --- @pytest.fixture def utc_now() -> datetime: """Get current UTC time.""" return datetime.now(timezone.utc)