"""Tests for the config module.""" from __future__ import annotations import subprocess import sys from unittest.mock import patch import pytest def test_config_defaults() -> None: """Test that config loads with expected default values.""" env_str = "" for k, v in { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", "CHAT_ENDPOINT_KEY": "test-key", "IMAGE_GEN_ENDPOINT_KEY": "test-image-key", "IMAGE_EDIT_ENDPOINT_KEY": "test-image-edit-key", "EMBEDDING_ENDPOINT_KEY": "test-embedding-key", "MAX_COMPLETION_TOKENS": "1000", "MAX_HISTORY_MESSAGES": "1000", "SIMILARITY_THRESHOLD": "0.7", "TOP_K_RESULTS": "5", "TTS_MODEL_PATH": "/tmp/test-model.onnx", "TTS_VOICES_PATH": "/tmp/test-voices.bin", "TTS_VOICE": "af_sarah", "TTS_SPEED": "1.0", "DB_PATH": ":memory:", }.items(): env_str += f'os.environ["{k}"] = "{v}"\n' code = f""" import os os.environ.clear() os.environ["PATH"] = "/usr/bin:/bin" {env_str} import vibe_bot.config assert vibe_bot.config.DISCORD_TOKEN == "test-token" assert vibe_bot.config.CHAT_ENDPOINT == "https://chat.example.com/v1" assert vibe_bot.config.IMAGE_GEN_ENDPOINT == "https://image.example.com/v1" assert vibe_bot.config.IMAGE_EDIT_ENDPOINT == "https://image-edit.example.com/v1" assert vibe_bot.config.EMBEDDING_ENDPOINT == "https://embedding.example.com/v1" assert vibe_bot.config.CHAT_MODEL == "test-chat-model" assert vibe_bot.config.IMAGE_GEN_MODEL == "test-image-model" assert vibe_bot.config.IMAGE_EDIT_MODEL == "test-image-edit-model" assert vibe_bot.config.EMBEDDING_MODEL == "test-embedding-model" assert vibe_bot.config.MAX_COMPLETION_TOKENS == 1000 assert vibe_bot.config.MAX_HISTORY_MESSAGES == 1000 assert vibe_bot.config.SIMILARITY_THRESHOLD == 0.7 assert vibe_bot.config.TOP_K_RESULTS == 5 assert vibe_bot.config.TTS_MODEL_PATH == "/tmp/test-model.onnx" assert vibe_bot.config.TTS_VOICES_PATH == "/tmp/test-voices.bin" assert vibe_bot.config.TTS_VOICE == "af_sarah" assert vibe_bot.config.TTS_SPEED == 1.0 print("OK") """ result = subprocess.run( # noqa: PLW1510 [sys.executable, "-c", code], capture_output=True, text=True, timeout=30, ) assert result.returncode == 0, f"Subprocess failed: {result.stderr}" def _run_config_check(env_vars: dict[str, str], expected_error: str) -> None: """Run a subprocess that imports config and calls validate_config(). The import itself must never raise; only validate_config() may raise the expected RuntimeError for the missing required setting. """ env_str = "" for k, v in env_vars.items(): env_str += f'os.environ["{k}"] = "{v}"\n' code = f""" import os os.environ.clear() os.environ["PATH"] = "/usr/bin:/bin" {env_str} try: import vibe_bot.config vibe_bot.config.validate_config() print("NO_ERROR") except RuntimeError as e: print(f"ERROR: {{e}}") except Exception as e: print(f"OTHER: {{type(e).__name__}}: {{e}}") """ result = subprocess.run( # noqa: PLW1510 [sys.executable, "-c", code], capture_output=True, text=True, timeout=30, ) output = result.stdout.strip() assert ( output.startswith("ERROR:") and expected_error in output ), f"Expected error '{expected_error}' but got: {output}" def test_config_missing_discord_token() -> None: """Test that RuntimeError is raised when DISCORD_TOKEN is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "DISCORD_TOKEN required") def test_config_missing_chat_endpoint() -> None: """Test that RuntimeError is raised when CHAT_ENDPOINT is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "CHAT_ENDPOINT required") def test_config_missing_image_gen_endpoint() -> None: """Test that RuntimeError is raised when IMAGE_GEN_ENDPOINT is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "IMAGE_GEN_ENDPOINT required") def test_config_missing_image_edit_endpoint() -> None: """Test that RuntimeError is raised when IMAGE_EDIT_ENDPOINT is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "IMAGE_EDIT_ENDPOINT required") def test_config_missing_embedding_endpoint() -> None: """Test that RuntimeError is raised when EMBEDDING_ENDPOINT is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "EMBEDDING_ENDPOINT required") def test_config_missing_chat_model() -> None: """Test that RuntimeError is raised when CHAT_MODEL is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "CHAT_MODEL required") def test_config_missing_image_gen_model() -> None: """Test that RuntimeError is raised when IMAGE_GEN_MODEL is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "IMAGE_GEN_MODEL required") def test_config_missing_image_edit_model() -> None: """Test that RuntimeError is raised when IMAGE_EDIT_MODEL is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "", "EMBEDDING_MODEL": "test-embedding-model", } _run_config_check(env, "IMAGE_EDIT_MODEL required") def test_config_missing_embedding_model() -> None: """Test that RuntimeError is raised when EMBEDDING_MODEL is missing.""" env: dict[str, str] = { "DISCORD_TOKEN": "test-token", "CHAT_ENDPOINT": "https://chat.example.com/v1", "IMAGE_GEN_ENDPOINT": "https://image.example.com/v1", "IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1", "EMBEDDING_ENDPOINT": "https://embedding.example.com/v1", "CHAT_MODEL": "test-chat-model", "IMAGE_GEN_MODEL": "test-image-model", "IMAGE_EDIT_MODEL": "test-image-edit-model", "EMBEDDING_MODEL": "", } _run_config_check(env, "EMBEDDING_MODEL required") REQUIRED_VARS = ( "DISCORD_TOKEN", "CHAT_ENDPOINT", "IMAGE_GEN_ENDPOINT", "IMAGE_EDIT_ENDPOINT", "EMBEDDING_ENDPOINT", "CHAT_MODEL", "IMAGE_GEN_MODEL", "IMAGE_EDIT_MODEL", "EMBEDDING_MODEL", ) def test_validate_config_passes_with_full_env() -> None: """With all required settings present, validate_config() is a no-op.""" from vibe_bot import config config.validate_config() @pytest.mark.parametrize("var_name", REQUIRED_VARS) def test_validate_config_missing_var(var_name: str) -> None: """Blanking any single required setting makes validate_config() raise.""" from vibe_bot import config with ( patch.object(config, var_name, ""), pytest.raises(RuntimeError, match=f"{var_name} required"), ): config.validate_config() def test_import_config_empty_env_no_raise_no_logging() -> None: """In an empty env the import succeeds and leaves the root logger unconfigured.""" code = """ import logging import os os.environ.clear() os.environ["PATH"] = "/usr/bin:/bin" import vibe_bot.config handlers = logging.getLogger().handlers assert handlers == [], f"config configured logging: {handlers}" print("OK") """ result = subprocess.run( # noqa: PLW1510 [sys.executable, "-c", code], capture_output=True, text=True, timeout=30, ) assert result.returncode == 0, f"Subprocess failed: {result.stderr}" assert "OK" in result.stdout def test_config_logging_exists() -> None: """Test that logging is configured in config module.""" from vibe_bot.config import logger assert logger is not None assert logger.name == "vibe_bot.config"