Various test fixes

This commit is contained in:
2026-08-17 11:35:40 -04:00
parent 2c61a04bcc
commit 5518f07234
10 changed files with 126 additions and 37 deletions
+36
View File
@@ -0,0 +1,36 @@
# AGENTS.md
Single Python package `vibe_bot`: a Discord bot (discord.py, `!` prefix) with SQLite RAG chat history, Kokoro TTS, and image gen/edit via OpenAI-compatible APIs. Python 3.13, managed with uv. Everything lives in `vibe_bot/`.
## Commands
- Setup: `uv sync --extra dev` — plain `uv sync` already installs the `dev` dependency group (ruff, pyright); the `dev` extra adds black/debugpy/mypy
- Run bot: `uv run python -m vibe_bot.main` — this logs the real bot into Discord with the token from `.env`; don't run it as a smoke test
- Tests: `uv run pytest vibe_bot/tests/ -v`; single test: `uv run pytest vibe_bot/tests/test_main.py::test_name`
- Checks: `uv run ruff check vibe_bot/`, `uv run mypy vibe_bot/` (strict), `uv run pyright vibe_bot/` (strict), `uv run black --check vibe_bot/`
- Container: `./build.sh` (podman). CI (`.gitea/workflows/build-push.yml`, Gitea) only builds/pushes the image on main/release — lint and tests are not gated; run them locally
## Setup requirements
- A repo-root `.env` is required even to run tests: `config.py` calls `load_dotenv()` and raises `RuntimeError` at import time if any required var is missing. Placeholder values suffice for the mocked suite. Never commit it.
- PortAudio is a required system library (kokoro-tts → sounddevice). Without it, ~60 tests in `test_main`/`test_tts` error at import time. On this host: `sudo dnf install portaudio` (the Containerfile installs `portaudio19-dev`).
- TTS needs `kokoro-v1.0.onnx` and `voices-v1.0.bin` in the repo root (baked into the container image). The bot runs without them; only `!speak` degrades.
## Test suite gotchas
- Baseline: everything passes except `test_llama_wrapper.py::test_chat_completion_think` and `::test_chat_completion_instruct`. Those two are unmocked live calls to the real `CHAT_ENDPOINT` from `.env` and fail without network access to that API.
- `test_config.py` hardcodes `sys.path.insert(0, "/var/home/ducoterra/Projects/vibe_discord_bots")` (a stale repo path). The test only passes because pytest's cwd fallback finds the package — run tests from the repo root.
- If `uv run <tool>` suddenly fails with `ModuleNotFoundError` or "bad interpreter", the `.venv` shebangs are stale from a repo move: `rm -rf .venv && uv sync --extra dev`.
## Code map
- `main.py` — entrypoint. The bot is created at module import (module-level `commands.Bot(...)`), `bot.run()` only under `__main__`. Custom-bot "commands" (`!<bot_name> ...`) are matched in `on_message` against the database, not registered with `bot.command`.
- `config.py` — env loading + import-time validation, voice catalog.
- `database.py` — `ChatDatabase` (embeddings stored as float32 blobs, cosine-similarity RAG, schema auto-migrates on startup) and `CustomBotManager`.
- `llama_wrapper.py` — thin OpenAI-compatible clients for chat / image gen / image edit / embeddings, each with its own endpoint, key, and model.
- `tools.py` — `get_channel_members` is a no-op LangChain `@tool` stub used only for name/description/schema; the real implementation is `get_channel_members_impl(channel)`, wired into the tool executor in `main.py`.
## Style
- All four checks (ruff, mypy strict, pyright strict, black) are declared gates and currently pass; keep new code clean under all of them.
- ruff is a dev dependency (installed via `uv add --dev`) with config in `pyproject.toml`; it runs with the default ruleset of the locked version.
+10 -1
View File
@@ -21,7 +21,6 @@ dependencies = [
[project.optional-dependencies]
dev = [
"pyright>=1.1.398",
"mypy>=1.17.0",
"black>=25.1.0",
"debugpy>=1.8.0",
@@ -56,7 +55,17 @@ reportPrivateUsage = false
line-length = 88
target-version = ["py313"]
[tool.ruff]
line-length = 88
target-version = "py313"
[tool.pytest.ini_options]
filterwarnings = [
"ignore::pytest.PytestUnraisableExceptionWarning",
]
[dependency-groups]
dev = [
"pyright>=1.1.409",
"ruff>=0.16.3",
]
Generated
+36 -1
View File
@@ -2015,6 +2015,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
]
[[package]]
name = "ruff"
version = "0.16.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" },
{ url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" },
{ url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" },
{ url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" },
{ url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" },
{ url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" },
{ url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" },
{ url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" },
{ url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" },
{ url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" },
{ url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" },
{ url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" },
{ url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" },
{ url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" },
]
[[package]]
name = "scikit-learn"
version = "1.8.0"
@@ -2461,7 +2486,12 @@ dev = [
{ name = "black" },
{ name = "debugpy" },
{ name = "mypy" },
]
[package.dev-dependencies]
dev = [
{ name = "pyright" },
{ name = "ruff" },
]
[package.metadata]
@@ -2476,7 +2506,6 @@ requires-dist = [
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.17.0" },
{ name = "numpy", specifier = ">=1.24.0" },
{ name = "openai", specifier = ">=2.24.0" },
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.398" },
{ name = "pytest", specifier = ">=9.0.2" },
{ name = "pytest-env", specifier = ">=1.5.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" },
@@ -2485,6 +2514,12 @@ requires-dist = [
]
provides-extras = ["dev"]
[package.metadata.requires-dev]
dev = [
{ name = "pyright", specifier = ">=1.1.409" },
{ name = "ruff", specifier = ">=0.16.3" },
]
[[package]]
name = "xxhash"
version = "3.7.0"
+1 -1
View File
@@ -504,7 +504,7 @@ _chat_db: ChatDatabase | None = None
def get_database() -> ChatDatabase:
"""Get or create the global database instance."""
global _chat_db # noqa: PLW0603
global _chat_db
if _chat_db is None:
_chat_db = ChatDatabase()
return _chat_db
+2 -1
View File
@@ -7,7 +7,8 @@ Allows custom endpoints for each of the above supported functions.
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Awaitable, Callable, cast
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, cast
import openai
import requests
+2 -2
View File
@@ -13,7 +13,6 @@ from discord import Message
from discord.ext import commands
from vibe_bot import llama_wrapper, tts
from vibe_bot.tts import DEFAULT_LANG
from vibe_bot.config import (
CHAT_ENDPOINT,
CHAT_ENDPOINT_KEY,
@@ -35,6 +34,7 @@ from vibe_bot.config import (
)
from vibe_bot.database import CustomBotManager, get_database
from vibe_bot.tools import get_channel_members, get_channel_members_impl
from vibe_bot.tts import DEFAULT_LANG
if TYPE_CHECKING:
from discord.ext.commands import Bot
@@ -771,7 +771,7 @@ async def retcon(ctx: CommandsContext[Bot], *, message: str) -> None:
for discord_image in ctx.message.attachments:
image_url = discord_image.url
try:
response = requests.get(image_url, timeout=30)
response = requests.get(image_url, timeout=30) # noqa: ASYNC210
image_data = response.content
except requests.RequestException as e:
logger.warning("Failed to download image from %s: %s", image_url, e)
+11 -7
View File
@@ -103,7 +103,7 @@ def custom_bot_manager(temp_db_path: str) -> CustomBotManager:
from vibe_bot.database import CustomBotManager
manager = CustomBotManager(db_path=temp_db_path)
return manager # noqa: RET504
return manager
@pytest.fixture
@@ -123,8 +123,8 @@ def mock_kokoro_tts() -> Generator[dict[str, Any]]:
return_value=mock_kokoro_instance,
),
patch("vibe_bot.tts.chunk_text", mock_chunk),
patch("vibe_bot.tts.process_chunk_sequential", mock_process),
):
with patch("vibe_bot.tts.process_chunk_sequential", mock_process):
yield {
"Kokoro": mock_kokoro,
"chunk_text": mock_chunk,
@@ -148,9 +148,11 @@ def mock_discord() -> Generator[dict[str, MagicMock]]:
mock_bot_instance.user.name = "test-bot"
mock_bot_instance.user.id = "123456789"
with patch("vibe_bot.main.discord") as mock_discord_module:
with patch("vibe_bot.main.commands", MagicMock()):
with patch("vibe_bot.main.commands.Bot", mock_bot_class):
with (
patch("vibe_bot.main.discord") as mock_discord_module,
patch("vibe_bot.main.commands", MagicMock()),
patch("vibe_bot.main.commands.Bot", mock_bot_class),
):
mock_bot_class.return_value = mock_bot_instance
mock_discord_module.Intents = mock_intents
mock_discord_module.Message = MagicMock
@@ -167,8 +169,10 @@ def mock_tts_engine() -> Generator[MagicMock]:
"""Provide a mock TTSEngine."""
mock_engine = MagicMock()
mock_engine.generate_audio.return_value = MagicMock()
with patch("vibe_bot.main.tts_engine", mock_engine):
with patch("vibe_bot.main.tts.TTSEngine", return_value=mock_engine):
with (
patch("vibe_bot.main.tts_engine", mock_engine),
patch("vibe_bot.main.tts.TTSEngine", return_value=mock_engine),
):
yield mock_engine
+2 -2
View File
@@ -68,7 +68,7 @@ assert vibe_bot.config.TTS_SPEED == 1.0
print("OK")
"""
result = subprocess.run( # noqa: PLW1510, S603
result = subprocess.run( # noqa: PLW1510
[sys.executable, "-c", code],
capture_output=True,
text=True,
@@ -99,7 +99,7 @@ except Exception as e:
print(f"OTHER: {{type(e).__name__}}: {{e}}")
"""
result = subprocess.run( # noqa: PLW1510, S603
result = subprocess.run( # noqa: PLW1510
[sys.executable, "-c", code],
capture_output=True,
text=True,
+3 -3
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import UTC
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -44,8 +45,8 @@ def mock_ctx_with_member() -> MagicMock:
ctx.author.activities = [mock_activity]
from datetime import datetime
ctx.author.joined_at = datetime(2024, 1, 15)
ctx.author.created_at = datetime(2023, 6, 1)
ctx.author.joined_at = datetime(2024, 1, 15, tzinfo=UTC)
ctx.author.created_at = datetime(2023, 6, 1, tzinfo=UTC)
ctx.channel.id = "channel-1"
ctx.guild.id = "guild-1"
ctx.message.id = "msg-1"
@@ -481,7 +482,6 @@ def test_speak_plain_with_mock_tts(
asyncio.run(main_module._speak_plain(mock_ctx, "hello world", mock_tts_engine))
from vibe_bot.config import TTS_SPEED, TTS_VOICE
from vibe_bot.tts import DEFAULT_LANG
mock_tts_engine.generate_audio.assert_called_once_with(
+5 -1
View File
@@ -8,6 +8,10 @@ import numpy as np
import pytest
class ChunkProcessingError(Exception):
"""Simulates a chunk processing failure in the fake Kokoro processor."""
def test_tts_engine_init(mock_kokoro_tts: MagicMock) -> None:
"""Test TTSEngine initialization."""
from vibe_bot.tts import TTSEngine
@@ -91,7 +95,7 @@ def test_generate_audio_chunk_failure(mock_kokoro_tts: MagicMock) -> None:
lang: str,
) -> tuple[np.ndarray, int]:
if chunk == "bad chunk":
raise Exception("processing error")
raise ChunkProcessingError("processing error")
return np.array([0.1, 0.2], dtype=np.float32), 24000
mock_kokoro_tts["chunk_text"].return_value = [