"""Shared test fixtures for vibe_bot tests.""" from __future__ import annotations import tempfile import warnings from collections.abc import Generator from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch import numpy as np import pytest from discord.ext import commands from vibe_bot.app import App, build_bot warnings.filterwarnings( "ignore", message="Exception ignored in.*FileIO.*Bad file descriptor", ) if TYPE_CHECKING: from vibe_bot.database import ChatDatabase, CustomBotManager @dataclass(frozen=True) class AppMocks: """The App under test plus its mock dependencies, for setup and asserts.""" app: App db: MagicMock manager: MagicMock registry: MagicMock chat: MagicMock image: MagicMock speech: MagicMock conversation: MagicMock @pytest.fixture def mock_ctx() -> MagicMock: """Create a mock Discord command context.""" ctx = MagicMock() ctx.author.name = "testuser" ctx.author.id = "12345" ctx.author.global_name = "Test User" ctx.author.nick = "tester" ctx.author.top_role.name = "@everyone" ctx.author.activities = [] ctx.author.joined_at = None ctx.author.created_at = None ctx.channel.id = "channel-1" ctx.guild.id = "guild-1" ctx.message.id = "msg-1" ctx.message.attachments = [] ctx.bot.user = MagicMock() ctx.bot.user.name = "test-bot" ctx.bot.user.id = "bot-123" ctx.send = AsyncMock() return ctx @pytest.fixture def app_mocks() -> AppMocks: """An App built entirely from mocks, with alfred in the bot cache.""" db = MagicMock() manager = MagicMock() manager.list_custom_bots.return_value = [ ("alfred", "british butler", "user123"), ] registry = MagicMock() chat = MagicMock() chat.handle = AsyncMock() image = MagicMock() image.generate = AsyncMock() image.edit = AsyncMock() speech = MagicMock() speech.speak = AsyncMock() conversation = MagicMock() conversation.run = AsyncMock() app = App( db=db, manager=manager, registry=registry, tts=MagicMock(), chat=chat, image=image, speech=speech, conversation=conversation, bot_cache={"alfred": ("british butler", "user123")}, ) return AppMocks(app, db, manager, registry, chat, image, speech, conversation) @pytest.fixture def bot(app_mocks: AppMocks) -> commands.Bot: """A real Bot built from the mock App (never connected).""" return build_bot(app_mocks.app) @pytest.fixture def temp_db_path() -> Generator[str]: """Provide a temporary SQLite database path.""" with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: path = f.name yield path Path(path).unlink(missing_ok=True) @pytest.fixture def mock_embedding() -> Generator[MagicMock]: """Provide a mock embedding function returning a fixed vector.""" vector: list[float] = [0.1] * 2048 with patch("vibe_bot.llm_client.embedding", return_value=vector) as mock: yield mock @pytest.fixture def chat_db( temp_db_path: str, mock_embedding: MagicMock, ) -> Generator[ChatDatabase]: """Provide a ChatDatabase instance with a temp database.""" from vibe_bot.database import ChatDatabase db = ChatDatabase(db_path=temp_db_path) yield db @pytest.fixture def custom_bot_manager(temp_db_path: str) -> CustomBotManager: """Provide a CustomBotManager instance with a temp database.""" from vibe_bot.database import CustomBotManager manager = CustomBotManager(db_path=temp_db_path) return manager @pytest.fixture def mock_kokoro_tts() -> Generator[dict[str, Any]]: """Provide mock Kokoro TTS components.""" mock_kokoro = MagicMock() mock_kokoro_instance = MagicMock() mock_chunk = MagicMock() mock_chunk.return_value = ["hello world", "this is a test"] mock_samples = np.array([0.1, 0.2, 0.3], dtype=np.float32) mock_process = MagicMock(return_value=(mock_samples, 24000)) with ( patch( "vibe_bot.tts.Kokoro", return_value=mock_kokoro_instance, ), patch("vibe_bot.tts.chunk_text", mock_chunk), patch("vibe_bot.tts.process_chunk_sequential", mock_process), ): yield { "Kokoro": mock_kokoro, "chunk_text": mock_chunk, "process_chunk_sequential": mock_process, "kokoro_instance": mock_kokoro_instance, "mock_samples": mock_samples, "mock_sr": 24000, }