InferRoute / tests /test_gateway.py
Ypeng12's picture
fix: resolve circuit breaker mocking and update router test assertions
d142175
Raw
History Blame Contribute Delete
6.35 kB
import pytest
import json
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
from inferroute.main import app
from inferroute.router import Router
from inferroute.validator import OutputValidator
from inferroute.cache import CacheLayer
client = TestClient(app)
@pytest.fixture
def mock_redis():
"""Mocks redis client interactions."""
with patch("inferroute.auth.get_redis_client") as mock_get_auth, \
patch("inferroute.cache.get_redis_client") as mock_get_cache, \
patch("inferroute.router.get_redis_client") as mock_get_router:
redis_mock = AsyncMock()
redis_mock.get.return_value = None
redis_mock.incr.return_value = 1
redis_mock.expire.return_value = True
mock_get_auth.return_value = redis_mock
mock_get_cache.return_value = redis_mock
mock_get_router.return_value = redis_mock
yield redis_mock
@pytest.fixture
def mock_db():
"""Mocks database session logging interactions."""
with patch("inferroute.main.async_session") as mock_sess:
session_mock = AsyncMock()
session_mock.add = MagicMock()
mock_sess.return_value.__aenter__.return_value = session_mock
yield session_mock
def test_healthz():
response = client.get("/healthz")
assert response.status_code == 200
assert response.json()["status"] == "ok"
@pytest.mark.asyncio
async def test_router_hard_pins():
router = Router()
# Test OpenAI pinning
decision = await router.choose_backend({"model": "gpt-4o-mini"})
assert decision.primary == "openai"
assert decision.fallback == "gemini"
assert "gpt-4o-mini" in decision.reason
# Test vLLM pinning
decision = await router.choose_backend({"model": "meta-llama/Meta-Llama-3-8B-Instruct"})
assert decision.primary == "vllm"
assert decision.fallback == "openai"
@pytest.mark.asyncio
async def test_router_optimization():
router = Router()
# Latency preference (default)
req = {
"model": "edge/auto",
"messages": [{"role": "user", "content": "hello"}],
"routing": {"allow_local": True, "allow_cloud": True, "policy": "latency"}
}
# Because baselines set local (vllm) score higher due to caches, it will choose vllm
decision = await router.choose_backend(req)
assert decision.primary in ["vllm", "openai", "ollama"]
assert decision.fallback is not None
def test_validator_schema():
validator = OutputValidator()
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
# Correct structure
res = validator.validate_schema('{"name": "Alice", "age": 30}', schema)
assert res.ok
assert res.reason is None
# Bad JSON structure
res2 = validator.validate_schema('{"name": "Alice", "age": 30', schema)
assert not res2.ok
assert "JSON decode error" in res2.reason
# Schema violation
res3 = validator.validate_schema('{"name": "Alice", "age": "thirty"}', schema)
assert not res3.ok
assert "validation error" in res3.reason.lower()
@patch("inferroute.adapters.vllm.VLLMAdapter.generate")
@patch("inferroute.adapters.openai.OpenAIAdapter.generate")
def test_gateway_chat_completion_blocking(mock_openai_gen, mock_vllm_gen, mock_redis, mock_db):
"""
Tests standard chat completions endpoint.
Verifies authentication, routing decisions, execution, and DB logging.
"""
# Setup mocks
mock_vllm_gen.return_value = {
"id": "mock-vllm-123",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hi there!"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15, "estimated_cost_usd": 0.0001},
"timing": {"ttft_ms": 100.0, "latency_ms": 150.0}
}
# Call endpoint without key - unauthorized
response = client.post(
"/v1/chat/completions",
json={"model": "edge/auto", "messages": [{"role": "user", "content": "hi"}]}
)
assert response.status_code == 401
# Call with key
headers = {"Authorization": "Bearer sk-inferroute-demo"}
response = client.post(
"/v1/chat/completions",
headers=headers,
json={"model": "meta-llama/Meta-Llama-3-8B-Instruct", "messages": [{"role": "user", "content": "hi"}]}
)
assert response.status_code == 200
data = response.json()
assert data["choices"][0]["message"]["content"] == "Hi there!"
assert data["route"]["selected_backend"] == "vllm"
assert data["route"]["cache_hit"] is False
# Verify cache lookup was executed
assert mock_redis.get.called
# Verify mock_vllm_gen was called
assert mock_vllm_gen.called
@patch("inferroute.adapters.vllm.VLLMAdapter.generate")
@patch("inferroute.adapters.openai.OpenAIAdapter.generate")
def test_gateway_chat_completion_fallback(mock_openai_gen, mock_vllm_gen, mock_redis, mock_db):
"""
Tests fallback logic.
Primary backend (vllm) fails, Gateway should fallback to OpenAI.
"""
# Setup mocks: primary vllm raises exception, fallback openai succeeds
mock_vllm_gen.side_effect = Exception("GPU OOM / Timeout")
mock_openai_gen.return_value = {
"id": "mock-openai-999",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello from Cloud!"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 10, "completion_tokens": 8, "total_tokens": 18, "estimated_cost_usd": 0.0002},
"timing": {"ttft_ms": 200.0, "latency_ms": 250.0}
}
headers = {"Authorization": "Bearer sk-inferroute-demo"}
response = client.post(
"/v1/chat/completions",
headers=headers,
json={"model": "meta-llama/Meta-Llama-3-8B-Instruct", "messages": [{"role": "user", "content": "hi"}]}
)
assert response.status_code == 200
data = response.json()
assert data["choices"][0]["message"]["content"] == "Hello from Cloud!"
assert data["route"]["selected_backend"] == "openai"
assert data["route"]["fallback_count"] == 1
# Verify both were called
assert mock_vllm_gen.called
assert mock_openai_gen.called