168 lines
4.5 KiB
Python
168 lines
4.5 KiB
Python
"""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,
|
|
}
|