From 733c2fcfdc0e425d02c3705b217b0dc7c7735f22 Mon Sep 17 00:00:00 2001 From: yucheng Date: Mon, 5 Oct 2026 09:52:18 +0000 Subject: [PATCH] test(guardrails): align logging_only_scope integration rows with abort-on-input-failure and encrypted params Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../_logging_only_scope_support.py | 36 +++++++++++++++++-- .../test_logging_only_scope_config.py | 13 +++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/tests/integration/observability/_logging_only_scope_support.py b/tests/integration/observability/_logging_only_scope_support.py index 75cacf22636..4c7d76d6564 100644 --- a/tests/integration/observability/_logging_only_scope_support.py +++ b/tests/integration/observability/_logging_only_scope_support.py @@ -31,6 +31,8 @@ from openai import AsyncOpenAI, OpenAI from openai.types.chat import ChatCompletionChunk from pydantic import JsonValue, TypeAdapter +from litellm.proxy.common_utils.callback_utils import CALLBACK_VAR_ENCRYPTED_PREFIX +from litellm.proxy.guardrails.guardrail_registry import decrypt_guardrail_litellm_params from tests.integration.cost_calculation.cost_tracking_case import JsonResponse, SseResponse JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) @@ -850,7 +852,7 @@ def _create_guardrail(candidate: Gateway, identity: str, params: Mapping[str, Js def _management_guardrail_rows(identity: str) -> tuple[dict[str, JsonValue], ...]: - return tuple( + rows: Final = tuple( object_value(row) for row in read_rows( "SELECT guardrail_id, guardrail_name, litellm_params, guardrail_info " @@ -858,6 +860,32 @@ def _management_guardrail_rows(identity: str) -> tuple[dict[str, JsonValue], ... (identity,), ) ) + return tuple({**row, "litellm_params": _decrypted_management_litellm_params(row["litellm_params"])} for row in rows) + + +def _decrypted_management_litellm_params(stored_value: JsonValue) -> dict[str, JsonValue]: + stored: Final = object_value(stored_value) + salt_key: Final = os.environ.get("LITELLM_SALT_KEY", "sk-integration-salt") + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setenv("LITELLM_SALT_KEY", salt_key) + decrypted: Final = JSON_OBJECT.validate_python(decrypt_guardrail_litellm_params(stored)) + assert _decryption_only_changes_encrypted_values(stored, decrypted) + return decrypted + + +def _decryption_only_changes_encrypted_values(stored: JsonValue, decrypted: JsonValue) -> bool: + if isinstance(stored, dict) and isinstance(decrypted, dict): + return stored.keys() == decrypted.keys() and all( + _decryption_only_changes_encrypted_values(value, decrypted[key]) for key, value in stored.items() + ) + if isinstance(stored, list) and isinstance(decrypted, list): + return len(stored) == len(decrypted) and all( + _decryption_only_changes_encrypted_values(stored_value, decrypted_value) + for stored_value, decrypted_value in zip(stored, decrypted) + ) + if isinstance(stored, str) and stored.startswith(CALLBACK_VAR_ENCRYPTED_PREFIX): + return isinstance(decrypted, str) and not decrypted.startswith(CALLBACK_VAR_ENCRYPTED_PREFIX) + return stored == decrypted def _drain_upstream(upstream_url: str) -> tuple[dict[str, JsonValue], ...]: @@ -865,8 +893,10 @@ def _drain_upstream(upstream_url: str) -> tuple[dict[str, JsonValue], ...]: response.raise_for_status() requests: Final = object_value(JSON_OBJECT.validate_python(response.json())).get("requests") assert isinstance(requests, list), response.text - _record_upstream_request_count(len(requests)) - return tuple(object_value(request) for request in requests) + observations: Final = tuple(object_value(request) for request in requests) + forwarded_requests: Final = tuple(request for request in observations if request.get("method", "POST") != "GET") + _record_upstream_request_count(len(forwarded_requests)) + return forwarded_requests def _json_contains_exact_string(value: JsonValue, expected: str) -> bool: diff --git a/tests/integration/observability/test_logging_only_scope_config.py b/tests/integration/observability/test_logging_only_scope_config.py index c3b82caf8d2..f2445c3a3a5 100644 --- a/tests/integration/observability/test_logging_only_scope_config.py +++ b/tests/integration/observability/test_logging_only_scope_config.py @@ -502,7 +502,7 @@ def test_F7_guardrail_ui_settings_classify_directional_scope_support(gateway: Ga "logging_only", "sideways", 200, - ("request", "response"), + ("request",), "logging_only", id="G3-yaml-logging-invalid-literal", ), @@ -1337,7 +1337,14 @@ def test_H10_management_patch_null_scope_restores_both_directions(gateway: Gatew def policy(request: Request) -> Reply: assert request.target == "/beta/litellm_basic_guardrail_api", request.target - return Reply(body=b'{"action":"BLOCKED","blocked_reason":"synthetic reset-scope monitor"}') + payload: Final = JSON_OBJECT.validate_json(request.body) + direction: Final = _direction(payload) + verdict: Final = ( + {"action": "NONE"} + if direction == "request" + else {"action": "BLOCKED", "blocked_reason": "synthetic reset-scope monitor"} + ) + return Reply(body=json.dumps(verdict).encode()) try: with wire_server(policy) as guardrail: @@ -1407,7 +1414,7 @@ def test_H10_management_patch_null_scope_restores_both_directions(gateway: Gatew assert tuple( (entry["guardrail_name"], entry["guardrail_mode"], entry["guardrail_status"]) for entry in entries ) == ( - (identity, "logging_only", "guardrail_intervened"), + (identity, "logging_only", "success"), (identity, "logging_only", "guardrail_intervened"), ), entries finally: