nomyo-router/test/conftest.py
alpha nerd 0b64a84e96
All checks were successful
PR Tests / test (pull_request) Successful in 1m0s
NYX Security Scan / nyx-scan (pull_request) Successful in 6m24s
fix: replace hardcoded tokendb path
2026-05-17 10:53:33 +02:00

233 lines
7.7 KiB
Python

"""
Test configuration for nomyo-router.
Run from project root:
pytest test/ -v
pytest test/ -m "not integration" # skip real-server tests
pytest test/ -m integration -v # only real-server tests
Environment variables:
NOMYO_TEST_OLLAMA Ollama endpoint (default: http://192.168.0.50:12434)
NOMYO_TEST_LLAMA llama-server endpoint (default: http://192.168.0.50:12434/v1)
NOMYO_TEST_MODEL_CHAT chat model to use (auto-discovered if unset)
NOMYO_TEST_EMBED_MODEL embedding model (auto-discovered if unset)
"""
import asyncio
import os
import ssl
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import aiohttp
import httpx
import pytest
_TEST_DIR = Path(__file__).parent
# Must be set before importing router so module-level Config.from_yaml + Config field
# defaults pick these up. db_path is intentionally absent from config_test.yaml so the
# env-var default wins — keeps tests portable across CI runners (Linux/macOS/Windows).
os.environ.setdefault("NOMYO_ROUTER_CONFIG_PATH", str(_TEST_DIR / "config_test.yaml"))
os.environ.setdefault(
"NOMYO_ROUTER_DB_PATH",
str(Path(tempfile.gettempdir()) / "nomyo_router_test_tokens.db"),
)
sys.path.insert(0, str(_TEST_DIR.parent))
import router # noqa: E402
TEST_OLLAMA = os.getenv("NOMYO_TEST_OLLAMA", "http://192.168.0.51:12434")
TEST_LLAMA = os.getenv("NOMYO_TEST_LLAMA", "http://192.168.0.51:12434/v1")
def pytest_configure(config):
config.addinivalue_line(
"markers",
"integration: tests that require a real backend at 192.168.0.50:12434",
)
# ── Config mocks ─────────────────────────────────────────────────────────────
@pytest.fixture
def mock_config():
"""Minimal config pointing at TEST_OLLAMA / TEST_LLAMA."""
cfg = MagicMock()
cfg.endpoints = [TEST_OLLAMA]
cfg.llama_server_endpoints = [TEST_LLAMA]
cfg.api_keys = {TEST_OLLAMA: "ollama", TEST_LLAMA: "llama"}
cfg.max_concurrent_connections = 2
cfg.router_api_key = None
cfg.cache_enabled = False
return cfg
@pytest.fixture
def mock_config_no_llama():
"""Config with Ollama only, no llama-server."""
cfg = MagicMock()
cfg.endpoints = [TEST_OLLAMA]
cfg.llama_server_endpoints = []
cfg.api_keys = {TEST_OLLAMA: "ollama"}
cfg.max_concurrent_connections = 2
cfg.router_api_key = None
cfg.cache_enabled = False
return cfg
@pytest.fixture
def mock_config_with_key():
"""Config with router_api_key set (enables auth middleware)."""
cfg = MagicMock()
cfg.endpoints = [TEST_OLLAMA]
cfg.llama_server_endpoints = []
cfg.api_keys = {}
cfg.max_concurrent_connections = 2
cfg.router_api_key = "test-secret-key"
cfg.cache_enabled = False
return cfg
# ── aiohttp session (used by fetch tests + choose_endpoint tests) ─────────────
@pytest.fixture
async def aio_session():
"""Real aiohttp session stored in app_state; intercepted by aioresponses."""
ssl_ctx = ssl.create_default_context()
conn = aiohttp.TCPConnector(ssl=ssl_ctx)
session = aiohttp.ClientSession(connector=conn)
router.app_state["session"] = session
# Clear caches to prevent test bleed
router._models_cache.clear()
router._loaded_models_cache.clear()
router._available_error_cache.clear()
router._loaded_error_cache.clear()
router._inflight_available_models.clear()
router._inflight_loaded_models.clear()
router._bg_refresh_available.clear()
router._bg_refresh_loaded.clear()
yield session
await session.close()
router.app_state["session"] = None
# ── Validation-only HTTP client (no real backend needed) ──────────────────────
@pytest.fixture
async def client(mock_config, tmp_path):
"""httpx client for validation/auth tests — no real backend calls made."""
from db import TokenDatabase
ssl_ctx = ssl.create_default_context()
conn = aiohttp.TCPConnector(ssl=ssl_ctx)
session = aiohttp.ClientSession(connector=conn)
db_inst = TokenDatabase(str(tmp_path / "test.db"))
await db_inst.init_db()
old_session = router.app_state.get("session")
old_db = router.db
router.app_state["session"] = session
router.db = db_inst
with patch.object(router, "config", mock_config):
transport = httpx.ASGITransport(app=router.app)
async with httpx.AsyncClient(
transport=transport, base_url="http://test", timeout=10.0
) as c:
yield c
await session.close()
router.app_state["session"] = old_session
router.db = old_db
@pytest.fixture
async def client_auth(mock_config_with_key, tmp_path):
"""httpx client with router_api_key configured (for auth middleware tests)."""
from db import TokenDatabase
ssl_ctx = ssl.create_default_context()
conn = aiohttp.TCPConnector(ssl=ssl_ctx)
session = aiohttp.ClientSession(connector=conn)
db_inst = TokenDatabase(str(tmp_path / "test_auth.db"))
await db_inst.init_db()
old_session = router.app_state.get("session")
old_db = router.db
router.app_state["session"] = session
router.db = db_inst
with patch.object(router, "config", mock_config_with_key):
transport = httpx.ASGITransport(app=router.app)
async with httpx.AsyncClient(
transport=transport, base_url="http://test", timeout=10.0
) as c:
yield c
await session.close()
router.app_state["session"] = old_session
router.db = old_db
# ── Integration client (full startup with real backend) ──────────────────────
@pytest.fixture(scope="module")
async def integration_client():
"""Full app startup pointing at the real test server."""
await router.startup_event()
transport = httpx.ASGITransport(app=router.app)
async with httpx.AsyncClient(
transport=transport,
base_url="http://test",
timeout=httpx.Timeout(60.0),
) as c:
yield c
await router.shutdown_event()
# ── Model discovery fixtures ──────────────────────────────────────────────────
@pytest.fixture(scope="module")
async def chat_model(integration_client):
"""Return a chat/generation model name available on the test server."""
env_model = os.getenv("NOMYO_TEST_MODEL_CHAT")
if env_model:
return env_model
resp = await integration_client.get("/api/tags")
if resp.status_code != 200:
pytest.skip("Cannot reach test server")
models = resp.json().get("models", [])
# Prefer small models for faster tests
for m in models:
name = m.get("name", "")
if any(x in name.lower() for x in ["0.5b", "1b", "3b", "1.5b", "2b"]):
return name
if models:
return models[0]["name"]
pytest.skip("No chat models available on test server")
@pytest.fixture(scope="module")
async def embed_model(integration_client):
"""Return an embedding model name available on the test server."""
env_model = os.getenv("NOMYO_TEST_EMBED_MODEL")
if env_model:
return env_model
resp = await integration_client.get("/api/tags")
if resp.status_code != 200:
pytest.skip("Cannot reach test server")
models = resp.json().get("models", [])
for m in models:
name = m.get("name", "")
if any(x in name.lower() for x in ["embed", "nomic", "minilm", "bge", "e5"]):
return name
pytest.skip("No embedding model available on test server")