Rebrand and personalize the bot as 'Bartender' - a companion for those who love deeply and feel intensely. Major changes: - Rename package: daemon_boyfriend -> loyal_companion - New default personality: Bartender - wise, steady, non-judgmental - Grief-aware system prompt (no toxic positivity, attachment-informed) - New relationship levels: New Face -> Close Friend progression - Bartender-style mood modifiers (steady presence) - New fact types: attachment_pattern, grief_context, coping_mechanism - Lower mood decay (0.05) for emotional stability - Higher fact extraction rate (0.4) - Bartender pays attention Updated all imports, configs, Docker files, and documentation.
310 lines
8.1 KiB
Python
310 lines
8.1 KiB
Python
"""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 loyal_companion.config import Settings
|
|
from loyal_companion.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 loyal_companion.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 loyal_companion.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 loyal_companion.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 loyal_companion.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 loyal_companion.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 loyal_companion.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)
|