From 7732eaa3b4fb10a032eedb6389fa23d4bee538f6 Mon Sep 17 00:00:00 2001 From: aniket-kardile Date: Fri, 21 Aug 2026 16:13:06 +0530 Subject: [PATCH] chore(guardrails): fix linting --- .../guardrail_hooks/test_singulr.py | 106 ++++++------------ 1 file changed, 33 insertions(+), 73 deletions(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_singulr.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_singulr.py index aa40d2f64c0..9eb1fa0d9e1 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_singulr.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_singulr.py @@ -168,6 +168,27 @@ class TestSingulrRequestPayload: mock_post.assert_not_called() assert result == {"texts": []} + @pytest.mark.asyncio + @pytest.mark.parametrize( + "extra_inputs", + [ + {"tools": [{"type": "function", "function": {"name": "delete_file", "description": "", "parameters": {}}}]}, + {"images": ["data:image/png;base64,abc123"]}, + ], + ids=["tools_alone", "images_alone"], + ) + async def test_tools_or_images_alone_still_trigger_the_api_call(self, singulr_guardrail, extra_inputs): + """Regression: a request with only tool definitions or only images and + no text must still be checked, not skipped for lack of a message.""" + resp = _make_response({"should_block": False}) + with patch.object(singulr_guardrail.async_handler, "post", return_value=resp) as mock_post: + await singulr_guardrail.apply_guardrail( + inputs={"texts": [], **extra_inputs}, + request_data={}, + input_type="request", + ) + mock_post.assert_called_once() + @pytest.mark.asyncio async def test_tools_are_forwarded(self, singulr_guardrail): """Regression: tool/function definitions are client-controlled and can @@ -189,33 +210,6 @@ class TestSingulrRequestPayload: sent_payload = mock_post.call_args.kwargs["json"] assert sent_payload["tools"] == tools - @pytest.mark.asyncio - async def test_tools_alone_still_triggers_the_api_call(self, singulr_guardrail): - """Regression: a request with tool definitions but no text or images - must still be checked, not skipped for lack of a message.""" - resp = _make_response({"should_block": False}) - tools = [{"type": "function", "function": {"name": "delete_file", "description": "", "parameters": {}}}] - with patch.object(singulr_guardrail.async_handler, "post", return_value=resp) as mock_post: - await singulr_guardrail.apply_guardrail( - inputs={"texts": [], "tools": tools}, - request_data={}, - input_type="request", - ) - mock_post.assert_called_once() - - @pytest.mark.asyncio - async def test_images_alone_still_triggers_the_api_call(self, singulr_guardrail): - """Regression: an image-only request (no text) must still be checked, - not skipped just because `texts` is empty.""" - resp = _make_response({"should_block": False}) - with patch.object(singulr_guardrail.async_handler, "post", return_value=resp) as mock_post: - await singulr_guardrail.apply_guardrail( - inputs={"texts": [], "images": ["data:image/png;base64,abc123"]}, - request_data={}, - input_type="request", - ) - mock_post.assert_called_once() - @pytest.mark.asyncio async def test_user_api_key_alias_is_forwarded_in_metadata(self, singulr_guardrail): """Regression: the alias must be sent as {"user_api_key_alias": }, @@ -329,8 +323,15 @@ class TestSingulrResponsePayload: class TestSingulrAllowAction: @pytest.mark.asyncio - async def test_should_block_false_returns_inputs_unchanged_on_request(self, singulr_guardrail): - resp = _make_response({"should_block": False}) + @pytest.mark.parametrize( + "guard_response", + [{"should_block": False}, {}], + ids=["should_block_false", "should_block_omitted"], + ) + async def test_should_block_falsy_returns_inputs_unchanged_on_request(self, singulr_guardrail, guard_response): + """should_block is optional on the wire; a response that omits it + entirely must be treated as allow, not block.""" + resp = _make_response(guard_response) inputs = {"texts": ["How do I reset my password?"]} with patch.object(singulr_guardrail.async_handler, "post", return_value=resp): result = await singulr_guardrail.apply_guardrail( @@ -340,20 +341,6 @@ class TestSingulrAllowAction: ) assert result is inputs - @pytest.mark.asyncio - async def test_should_block_none_returns_inputs_unchanged_on_request(self, singulr_guardrail): - """should_block is optional on the wire; a response that omits it - entirely must be treated as allow, not block.""" - resp = _make_response({}) - inputs = {"texts": ["hi"]} - with patch.object(singulr_guardrail.async_handler, "post", return_value=resp): - result = await singulr_guardrail.apply_guardrail( - inputs=inputs, - request_data={}, - input_type="request", - ) - assert result is inputs - @pytest.mark.asyncio async def test_should_block_false_returns_inputs_unchanged_on_response(self, singulr_guardrail): resp = _make_response({"should_block": False}) @@ -436,21 +423,6 @@ class TestSingulrMcpRequest: assert sent_payload["mcp_server_name"] == "docs-server" assert result == {"texts": []} - @pytest.mark.asyncio - async def test_mcp_request_ignores_texts_and_always_calls_api(self, singulr_guardrail): - """Unlike the plain text-message path, an MCP tool call has no - `texts`/`images` gate: it must always be checked even with empty - inputs, since the tool name/arguments alone are the payload.""" - resp = _make_response({"should_block": False}) - request_data = {"mcp_tool_name": "delete_file", "mcp_arguments": {}} - with patch.object(singulr_guardrail.async_handler, "post", return_value=resp) as mock_post: - await singulr_guardrail.apply_guardrail( - inputs={"texts": []}, - request_data=request_data, - input_type="request", - ) - mock_post.assert_called_once() - @pytest.mark.asyncio async def test_mcp_request_should_block_true_raises(self, singulr_guardrail): resp = _make_response({"should_block": True, "blocking_due_to": "Disallowed tool"}) @@ -577,9 +549,7 @@ class TestSingulrLoggingHook: return "" with patch.object(singulr_guardrail.async_handler, "post", return_value=resp) as mock_post: - await singulr_guardrail.async_logging_hook( - kwargs={}, result=Unserializable(), call_type="acompletion" - ) + await singulr_guardrail.async_logging_hook(kwargs={}, result=Unserializable(), call_type="acompletion") response_payload = mock_post.call_args.kwargs["json"] assert response_payload["response"] == "" @@ -643,7 +613,7 @@ class TestSingulrLoggingHook: class TestSingulrRequestWiring: @pytest.mark.asyncio - async def test_sends_configured_timeout(self): + async def test_sends_configured_timeout_and_calls_the_guard_endpoint(self): """litellm_params.timeout must reach the httpx call so operators can tighten or loosen the latency budget instead of being stuck with a hardcoded 30s regardless of configuration.""" @@ -659,17 +629,7 @@ class TestSingulrRequestWiring: request_data={}, input_type="request", ) - assert mock_post.call_args.kwargs["timeout"] == 5.0 - - @pytest.mark.asyncio - async def test_calls_the_guard_endpoint(self, singulr_guardrail): - resp = _make_response({"should_block": False}) - with patch.object(singulr_guardrail.async_handler, "post", return_value=resp) as mock_post: - await singulr_guardrail.apply_guardrail( - inputs={"texts": ["test"]}, - request_data={}, - input_type="request", - ) + assert mock_post.call_args.kwargs["timeout"] == 5.0 assert mock_post.call_args.kwargs["url"] == "https://api.test.singulr.ai/api/v1/ai-gateway/litellm"