test(caching): cover semantic cache isolation guards

This commit is contained in:
user
2026-05-01 11:26:03 -07:00
parent ae9b63c468
commit 1c19bdda79
5 changed files with 205 additions and 37 deletions
+2 -2
View File
@@ -59,7 +59,7 @@ def _normalize_operation_ids(paths: Dict[str, Dict]) -> None:
break
def generate_snapshot() -> Dict[str, Dict]:
def generate_snapshot() -> Dict[str, Dict]: # pragma: no cover
import importlib
from fastapi.openapi.utils import get_openapi
@@ -100,7 +100,7 @@ def generate_snapshot() -> Dict[str, Dict]:
return fragments
if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
fragments = generate_snapshot()
SNAPSHOT_FILE.write_text(json.dumps(fragments, indent=2, sort_keys=True) + "\n")
sys.stdout.write(f"wrote {len(fragments)} feature fragments to {SNAPSHOT_FILE}\n")
@@ -204,6 +204,39 @@ def test_qdrant_semantic_cache_rejects_unscoped_cache_hit():
assert result is None
def test_qdrant_semantic_cache_payload_index_failure_is_non_blocking():
from litellm.caching.qdrant_semantic_cache import QdrantSemanticCache
qdrant_cache = QdrantSemanticCache.__new__(QdrantSemanticCache)
qdrant_cache.qdrant_api_base = "http://test.qdrant.local"
qdrant_cache.collection_name = "test_collection"
qdrant_cache.headers = {"Content-Type": "application/json"}
qdrant_cache.sync_client = MagicMock()
response = MagicMock()
response.status_code = 400
response.text = "bad index"
qdrant_cache.sync_client.put.return_value = response
qdrant_cache._ensure_cache_key_payload_index()
qdrant_cache.sync_client.put.assert_called_once()
def test_qdrant_semantic_cache_payload_index_exception_is_non_blocking():
from litellm.caching.qdrant_semantic_cache import QdrantSemanticCache
qdrant_cache = QdrantSemanticCache.__new__(QdrantSemanticCache)
qdrant_cache.qdrant_api_base = "http://test.qdrant.local"
qdrant_cache.collection_name = "test_collection"
qdrant_cache.headers = {"Content-Type": "application/json"}
qdrant_cache.sync_client = MagicMock()
qdrant_cache.sync_client.put.side_effect = Exception("boom")
qdrant_cache._ensure_cache_key_payload_index()
qdrant_cache.sync_client.put.assert_called_once()
def test_qdrant_semantic_cache_get_cache_miss():
"""
Test QDRANT semantic cache get method when there's a cache miss.
@@ -225,6 +225,52 @@ def test_redis_semantic_cache_uses_isolated_index_for_old_schema(monkeypatch):
]
def test_redis_semantic_cache_reraises_unexpected_index_error():
from litellm.caching.redis_semantic_cache import RedisSemanticCache
redis_semantic_cache = RedisSemanticCache.__new__(RedisSemanticCache)
redis_semantic_cache.distance_threshold = 0.2
semantic_cache_mock = MagicMock(side_effect=ValueError("connection failed"))
with pytest.raises(ValueError, match="connection failed"):
redis_semantic_cache._init_semantic_cache(
semantic_cache_cls=semantic_cache_mock,
index_name="existing_index",
redis_url="redis://localhost:6379",
cache_vectorizer=MagicMock(),
)
def test_redis_semantic_cache_matches_bytes_cache_key():
from litellm.caching.redis_semantic_cache import RedisSemanticCache
redis_semantic_cache = RedisSemanticCache.__new__(RedisSemanticCache)
assert redis_semantic_cache._cache_hit_matches_key(
cache_hit={RedisSemanticCache.CACHE_KEY_FIELD_NAME: b"test_key"},
key="test_key",
)
def test_redis_semantic_cache_builds_filter_expression(monkeypatch):
class FakeTag:
def __init__(self, field_name):
self.field_name = field_name
def __eq__(self, value):
return (self.field_name, value)
with patch.dict("sys.modules", {"redisvl.query.filter": MagicMock(Tag=FakeTag)}):
from litellm.caching.redis_semantic_cache import RedisSemanticCache
redis_semantic_cache = RedisSemanticCache.__new__(RedisSemanticCache)
assert redis_semantic_cache._get_cache_key_filter_expression("test_key") == (
RedisSemanticCache.CACHE_KEY_FIELD_NAME,
"test_key",
)
@pytest.mark.asyncio
async def test_redis_semantic_cache_async_get_cache(monkeypatch):
# Mock the redisvl import
@@ -289,6 +335,54 @@ async def test_redis_semantic_cache_async_get_cache(monkeypatch):
)
@pytest.mark.asyncio
async def test_redis_semantic_cache_async_get_cache_rejects_unscoped_hit(monkeypatch):
semantic_cache_mock = MagicMock()
custom_vectorizer_mock = MagicMock()
with patch.dict(
"sys.modules",
{
"redisvl.extensions.llmcache": MagicMock(SemanticCache=semantic_cache_mock),
"redisvl.utils.vectorize": MagicMock(
CustomTextVectorizer=custom_vectorizer_mock
),
},
):
from litellm.caching.redis_semantic_cache import RedisSemanticCache
monkeypatch.setenv("REDIS_HOST", "localhost")
monkeypatch.setenv("REDIS_PORT", "6379")
monkeypatch.setenv("REDIS_PASSWORD", "test_password")
redis_semantic_cache = RedisSemanticCache(similarity_threshold=0.8)
redis_semantic_cache.llmcache.acheck = AsyncMock(
return_value=[
{
"prompt": "What is the capital of France?",
"response": '{"content": "Paris"}',
"vector_distance": 0.1,
}
]
)
redis_semantic_cache._get_async_embedding = AsyncMock(
return_value=[0.1, 0.2, 0.3]
)
with patch.object(
redis_semantic_cache,
"_get_cache_key_filter_expression",
return_value="cache-key-filter",
):
result = await redis_semantic_cache.async_get_cache(
key="test_key",
messages=[{"content": "What is the capital of France?"}],
metadata={},
)
assert result is None
@pytest.mark.asyncio
async def test_redis_semantic_cache_async_set_cache_stores_cache_key_filter(
monkeypatch,
@@ -1,35 +0,0 @@
from litellm.proxy._lazy_openapi_snapshot import _normalize_operation_ids
def test_normalize_operation_ids_uses_each_http_method():
paths = {
"/proxy/{endpoint}": {
"delete": {"operationId": "proxy_route_proxy__endpoint__put"},
"get": {"operationId": "proxy_route_proxy__endpoint__put"},
"post": {"operationId": "proxy_route_proxy__endpoint__put"},
"put": {"operationId": "proxy_route_proxy__endpoint__put"},
}
}
_normalize_operation_ids(paths)
operations = paths["/proxy/{endpoint}"]
assert operations["delete"]["operationId"] == "proxy_route_proxy__endpoint__delete"
assert operations["get"]["operationId"] == "proxy_route_proxy__endpoint__get"
assert operations["post"]["operationId"] == "proxy_route_proxy__endpoint__post"
assert operations["put"]["operationId"] == "proxy_route_proxy__endpoint__put"
def test_normalize_operation_ids_preserves_custom_ids():
paths = {
"/proxy/{endpoint}": {
"get": {"operationId": "custom_operation"},
"post": {"operationId": "custom_operation"},
}
}
_normalize_operation_ids(paths)
operations = paths["/proxy/{endpoint}"]
assert operations["get"]["operationId"] == "custom_operation"
assert operations["post"]["operationId"] == "custom_operation"
@@ -0,0 +1,76 @@
from litellm.proxy import _lazy_openapi_snapshot as snapshot_module
def test_load_snapshot_returns_none_when_missing(monkeypatch, tmp_path):
monkeypatch.setattr(snapshot_module, "SNAPSHOT_FILE", tmp_path / "missing.json")
assert snapshot_module.load_snapshot() is None
def test_load_snapshot_reads_json(monkeypatch, tmp_path):
snapshot_file = tmp_path / "snapshot.json"
snapshot_file.write_text('{"mcp": {"paths": {}}}')
monkeypatch.setattr(snapshot_module, "SNAPSHOT_FILE", snapshot_file)
assert snapshot_module.load_snapshot() == {"mcp": {"paths": {}}}
def test_load_snapshot_returns_none_for_invalid_json(monkeypatch, tmp_path):
snapshot_file = tmp_path / "snapshot.json"
snapshot_file.write_text("{")
monkeypatch.setattr(snapshot_module, "SNAPSHOT_FILE", snapshot_file)
assert snapshot_module.load_snapshot() is None
def test_normalize_operation_ids_uses_each_http_method():
paths = {
"/proxy/{endpoint}": {
"delete": {"operationId": "proxy_route_proxy__endpoint__put"},
"get": {"operationId": "proxy_route_proxy__endpoint__put"},
"post": {"operationId": "proxy_route_proxy__endpoint__put"},
"put": {"operationId": "proxy_route_proxy__endpoint__put"},
}
}
snapshot_module._normalize_operation_ids(paths)
operations = paths["/proxy/{endpoint}"]
assert operations["delete"]["operationId"] == "proxy_route_proxy__endpoint__delete"
assert operations["get"]["operationId"] == "proxy_route_proxy__endpoint__get"
assert operations["post"]["operationId"] == "proxy_route_proxy__endpoint__post"
assert operations["put"]["operationId"] == "proxy_route_proxy__endpoint__put"
def test_normalize_operation_ids_preserves_custom_ids():
paths = {
"/proxy/{endpoint}": {
"get": {"operationId": "custom_operation"},
"post": {"operationId": "custom_operation"},
}
}
snapshot_module._normalize_operation_ids(paths)
operations = paths["/proxy/{endpoint}"]
assert operations["get"]["operationId"] == "custom_operation"
assert operations["post"]["operationId"] == "custom_operation"
def test_normalize_operation_ids_skips_invalid_entries():
paths = {
"/not-a-dict": "skip",
"/no-http-methods": {"parameters": []},
"/invalid-operation": {
"get": ["skip"],
"post": {"operationId": 123},
"parameters": [],
},
}
snapshot_module._normalize_operation_ids(paths)
assert paths["/not-a-dict"] == "skip"
assert paths["/no-http-methods"] == {"parameters": []}
assert paths["/invalid-operation"]["get"] == ["skip"]
assert paths["/invalid-operation"]["post"] == {"operationId": 123}