litellm/tests/unit/proxy/policy_engine/test_policy_registry.py
Mateo Wang d80f8c28ca
refactor(types): replace Any with proven types in 137 files (#44478)
* refactor(types): prove runtime types at harness, search, rag and client boundaries

Replace Any with adapter-validated types in the litellm.agent() harness, the
search provider transformations, RAG ingestion and query, the vector store
pre-call hook and registry, the galileo and opik logging integrations and the
proxy client CLI. Each boundary gets unit tests for well-formed and malformed
payloads.

* chore(typing): prove types at more provider boundaries and restore search transformations

Second pass over a2a, embedding, rerank, image, audio and small provider
modules. Search transformations go back to their previous form because
validating their response bodies would change the proxy status for
malformed upstream bodies from 400 to 500.

* refactor(types): prove types at logging, files, rerank, image, audio and management boundaries

Replace Any with validated or annotation-only types in 37 more files: logging
integrations, token counters, provider files/rerank/image generation/audio
transcription transformations, pass-through logging handlers and management
endpoints. No proxy HTTP status or error type changes.

* refactor(types): prove types at repository, spend, files and router boundaries

Replace Any with repository table accessors, validated mappings and
annotation-only types in 33 more files: Prisma repositories, the enterprise
batch and responses cost checkers, budget reservation, files endpoints,
management endpoints, the policy registry, the adaptive and complexity routers
and the secret managers. No proxy HTTP status or error type changes.

* test(types): run the aiohttp transformation test in-process and cover repository row conversion

The aiohttp chat transformation test no longer starts a server. It feeds the
transformation a response whose json() returns the body under test.

The proxy unit shards now exercise stored model rows whose params are JSON
strings and the object permission create and update paths.
2026-10-05 11:01:45 -07:00

154 lines
5.5 KiB
Python

import json
from collections.abc import Mapping
from datetime import datetime, timezone
from types import SimpleNamespace
import pytest
from litellm.proxy.policy_engine.policy_registry import PolicyRegistry
from litellm.types.proxy.policy_engine import PolicyCreateRequest, PolicyUpdateRequest
_NOW = datetime(2026, 1, 1, tzinfo=timezone.utc)
_REQUESTED_PIPELINE = {"mode": "pre_call", "steps": [{"guardrail": "pii-guard", "on_fail": "next"}]}
_STORED_PIPELINE = {
"mode": "pre_call",
"steps": [
{
"guardrail": "pii-guard",
"on_fail": "next",
"on_pass": "allow",
"on_error": None,
"pass_data": False,
"modify_response_message": None,
}
],
}
_MALFORMED_PIPELINES = [
pytest.param({"mode": "pre_call", "steps": []}, "steps", id="no-steps"),
pytest.param({"mode": "pre_call"}, "steps", id="steps-missing"),
pytest.param({"steps": [{"guardrail": "pii-guard"}]}, "mode", id="mode-missing"),
pytest.param({"mode": "during_call", "steps": [{"guardrail": "pii-guard"}]}, "mode", id="unknown-mode"),
pytest.param(
{"mode": "pre_call", "steps": [{"guardrail": "pii-guard"}], "order": "strict"}, "order", id="unknown-key"
),
pytest.param({"mode": "pre_call", "steps": "pii-guard"}, "steps", id="steps-not-a-list"),
]
class _PolicyTable:
def __init__(self) -> None:
self.writes: list[Mapping[str, object]] = []
def _row(self, data: Mapping[str, object]) -> SimpleNamespace:
pipeline = data.get("pipeline")
return SimpleNamespace(
policy_id="policy-1",
policy_name=data.get("policy_name", "pii-policy"),
version_number=1,
version_status=data.get("version_status", "draft"),
parent_version_id=None,
is_latest=True,
published_at=None,
production_at=None,
inherit=None,
description=None,
guardrails_add=data.get("guardrails_add", []),
guardrails_remove=data.get("guardrails_remove", []),
condition=None,
pipeline=json.loads(pipeline) if isinstance(pipeline, str) else None,
created_at=_NOW,
updated_at=_NOW,
created_by=None,
updated_by=None,
)
async def create(self, data: Mapping[str, object]) -> SimpleNamespace:
self.writes.append(data)
return self._row(data)
async def find_unique(self, where: Mapping[str, object]) -> SimpleNamespace:
return self._row({"policy_name": "pii-policy", "version_status": "draft"})
async def update(self, where: Mapping[str, object], data: Mapping[str, object]) -> SimpleNamespace:
self.writes.append(data)
return self._row(data)
def _prisma_client(table: _PolicyTable) -> SimpleNamespace:
return SimpleNamespace(db=SimpleNamespace(litellm_policytable=table))
@pytest.mark.asyncio
async def test_add_policy_to_db_stores_the_pipeline_with_step_defaults_filled_in():
table = _PolicyTable()
registry = PolicyRegistry()
response = await registry.add_policy_to_db(
PolicyCreateRequest(policy_name="pii-policy", guardrails_add=["pii-guard"], pipeline=_REQUESTED_PIPELINE),
_prisma_client(table),
)
assert [json.loads(str(write["pipeline"])) for write in table.writes] == [_STORED_PIPELINE]
assert response.pipeline == _STORED_PIPELINE
stored_policy = registry.get_policy("pii-policy")
assert stored_policy is not None
assert stored_policy.pipeline is not None
assert stored_policy.pipeline.model_dump() == _STORED_PIPELINE
@pytest.mark.asyncio
async def test_update_policy_in_db_stores_the_pipeline_with_step_defaults_filled_in():
table = _PolicyTable()
response = await PolicyRegistry().update_policy_in_db(
"policy-1",
PolicyUpdateRequest(pipeline=_REQUESTED_PIPELINE),
_prisma_client(table),
)
assert [json.loads(str(write["pipeline"])) for write in table.writes] == [_STORED_PIPELINE]
assert response.pipeline == _STORED_PIPELINE
@pytest.mark.parametrize(("pipeline", "rejected_field"), _MALFORMED_PIPELINES)
@pytest.mark.asyncio
async def test_add_policy_to_db_rejects_a_malformed_pipeline_before_writing(
pipeline: dict[str, object], rejected_field: str
):
table = _PolicyTable()
registry = PolicyRegistry()
with pytest.raises(Exception, match="Error adding policy to DB: 1 validation error") as raised:
await registry.add_policy_to_db(
PolicyCreateRequest(policy_name="pii-policy", pipeline=pipeline),
_prisma_client(table),
)
assert str(raised.value).startswith(
f"Error adding policy to DB: 1 validation error for GuardrailPipeline\n{rejected_field}\n"
)
assert table.writes == []
assert registry.get_policy("pii-policy") is None
@pytest.mark.parametrize(("pipeline", "rejected_field"), _MALFORMED_PIPELINES)
@pytest.mark.asyncio
async def test_update_policy_in_db_rejects_a_malformed_pipeline_before_writing(
pipeline: dict[str, object], rejected_field: str
):
table = _PolicyTable()
with pytest.raises(Exception, match="Error updating policy in DB: 1 validation error") as raised:
await PolicyRegistry().update_policy_in_db(
"policy-1",
PolicyUpdateRequest(pipeline=pipeline),
_prisma_client(table),
)
assert str(raised.value).startswith(
f"Error updating policy in DB: 1 validation error for GuardrailPipeline\n{rejected_field}\n"
)
assert table.writes == []