diff --git a/src/models/rlsapi/requests.py b/src/models/rlsapi/requests.py index 1ee91c500..7c459c0cc 100644 --- a/src/models/rlsapi/requests.py +++ b/src/models/rlsapi/requests.py @@ -31,6 +31,7 @@ class RlsapiV1Attachment(ConfigurationBase): contents: str = Field( default="", + max_length=65_536, description="File contents read on client", examples=["# Configuration file\nkey=value"], ) @@ -50,6 +51,7 @@ class RlsapiV1Terminal(ConfigurationBase): output: str = Field( default="", + max_length=65_536, description="Terminal output from client", examples=["bash: command not found", "Permission denied"], ) @@ -129,6 +131,7 @@ class RlsapiV1Context(ConfigurationBase): stdin: str = Field( default="", + max_length=65_536, description="Redirect input from stdin", examples=["piped input from previous command"], ) @@ -173,6 +176,7 @@ class RlsapiV1InferRequest(ConfigurationBase): question: str = Field( ..., min_length=1, + max_length=10_240, description="User question", examples=["How do I list files?", "How do I configure SELinux?"], ) diff --git a/tests/integration/endpoints/test_rlsapi_v1_integration.py b/tests/integration/endpoints/test_rlsapi_v1_integration.py index 8963bfdf4..3d9ab16bb 100644 --- a/tests/integration/endpoints/test_rlsapi_v1_integration.py +++ b/tests/integration/endpoints/test_rlsapi_v1_integration.py @@ -13,6 +13,7 @@ import pytest from fastapi import HTTPException, status +from fastapi.testclient import TestClient from llama_stack_client import APIConnectionError from pytest_mock import MockerFixture @@ -494,3 +495,22 @@ async def test_rlsapi_v1_infer_skip_rag( auth=test_auth, ) assert isinstance(response, RlsapiV1InferResponse) + + +@pytest.mark.parametrize( + "json", + ( + ({"question": "?" * 10_241}), + ({"question": "Q", "context": {"stdin": "a" * 65_537}}), + ({"question": "Q", "context": {"attachments": {"contents": "A" * 65_537}}}), + ({"question": "Q", "context": {"terminal": {"output": "T" * 65_537}}}), + ), + ids=["question", "stdin", "attachment_contents", "terminal_output"], +) +def test_infer_size_limit(integration_http_client: TestClient, json) -> None: + """Test that a field exceeding limit is rejected.""" + response = integration_http_client.post("/v1/infer", json=json) + detail = response.json()["detail"] + + assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT + assert "string_too_long" in {item["type"] for item in detail} diff --git a/tests/unit/app/endpoints/test_rlsapi_v1.py b/tests/unit/app/endpoints/test_rlsapi_v1.py index c2c567668..b2527a686 100644 --- a/tests/unit/app/endpoints/test_rlsapi_v1.py +++ b/tests/unit/app/endpoints/test_rlsapi_v1.py @@ -309,7 +309,6 @@ def test_config_error_503_matches_llm_error_503_shape( # --- Test retrieve_simple_response --- -@pytest.mark.asyncio async def test_retrieve_simple_response_success( mock_configuration: AppConfig, mock_llm_response: None ) -> None: @@ -320,7 +319,6 @@ async def test_retrieve_simple_response_success( assert response == "This is a test LLM response." -@pytest.mark.asyncio async def test_retrieve_simple_response_empty_output( mock_configuration: AppConfig, mock_empty_llm_response: None ) -> None: @@ -331,7 +329,6 @@ async def test_retrieve_simple_response_empty_output( assert response == "" -@pytest.mark.asyncio async def test_retrieve_simple_response_api_connection_error( mock_configuration: AppConfig, mock_api_connection_error: None ) -> None: @@ -384,7 +381,6 @@ def test_get_rh_identity_context_with_empty_values(mocker: MockerFixture) -> Non # --- Test infer_endpoint --- -@pytest.mark.asyncio async def test_infer_minimal_request( mocker: MockerFixture, mock_configuration: AppConfig, @@ -409,7 +405,6 @@ async def test_infer_minimal_request( assert check_suid(response.data.request_id) -@pytest.mark.asyncio async def test_infer_full_context_request( mocker: MockerFixture, mock_configuration: AppConfig, @@ -441,7 +436,6 @@ async def test_infer_full_context_request( assert response.data.request_id -@pytest.mark.asyncio async def test_infer_generates_unique_request_ids( mocker: MockerFixture, mock_configuration: AppConfig, @@ -469,7 +463,6 @@ async def test_infer_generates_unique_request_ids( assert response1.data.request_id != response2.data.request_id -@pytest.mark.asyncio async def test_infer_api_connection_error_returns_503( mocker: MockerFixture, mock_configuration: AppConfig, @@ -492,7 +485,6 @@ async def test_infer_api_connection_error_returns_503( assert exc_info.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE -@pytest.mark.asyncio async def test_infer_empty_llm_response_returns_fallback( mocker: MockerFixture, mock_configuration: AppConfig, @@ -517,7 +509,6 @@ async def test_infer_empty_llm_response_returns_fallback( # --- Test Splunk integration --- -@pytest.mark.asyncio async def test_infer_queues_splunk_event_on_success( mocker: MockerFixture, mock_configuration: AppConfig, @@ -542,7 +533,6 @@ async def test_infer_queues_splunk_event_on_success( assert call_args[0][2] == "infer_with_llm" -@pytest.mark.asyncio async def test_infer_queues_splunk_error_event_on_failure( mocker: MockerFixture, mock_configuration: AppConfig, @@ -567,7 +557,6 @@ async def test_infer_queues_splunk_error_event_on_failure( assert call_args[0][2] == "infer_error" -@pytest.mark.asyncio async def test_infer_splunk_event_includes_rh_identity_context( mocker: MockerFixture, mock_configuration: AppConfig, @@ -638,7 +627,6 @@ def _setup_responses_mock_with_capture( return mock_create -@pytest.mark.asyncio async def test_retrieve_simple_response_passes_tools( mocker: MockerFixture, mock_configuration: AppConfig ) -> None: @@ -660,7 +648,6 @@ async def test_retrieve_simple_response_passes_tools( assert call_kwargs["tools"] == tools -@pytest.mark.asyncio async def test_retrieve_simple_response_defaults_to_empty_tools( mocker: MockerFixture, mock_configuration: AppConfig ) -> None: @@ -674,7 +661,6 @@ async def test_retrieve_simple_response_defaults_to_empty_tools( assert call_kwargs["tools"] == [] -@pytest.mark.asyncio async def test_infer_endpoint_calls_get_mcp_tools( mocker: MockerFixture, mock_configuration: AppConfig, @@ -704,7 +690,6 @@ async def test_infer_endpoint_calls_get_mcp_tools( ) -@pytest.mark.asyncio async def test_infer_generic_runtime_error_reraises( mocker: MockerFixture, mock_configuration: AppConfig, @@ -725,7 +710,6 @@ async def test_infer_generic_runtime_error_reraises( ) -@pytest.mark.asyncio async def test_infer_generic_runtime_error_records_failure( mocker: MockerFixture, mock_configuration: AppConfig, diff --git a/tests/unit/models/rlsapi/test_requests.py b/tests/unit/models/rlsapi/test_requests.py index cd13df988..d526571d2 100644 --- a/tests/unit/models/rlsapi/test_requests.py +++ b/tests/unit/models/rlsapi/test_requests.py @@ -594,3 +594,33 @@ def test_priority_order(self, make_request: Any) -> None: ) result = request.get_input_source() assert result == "Q\n\nS\n\nA\n\nT" + + +@pytest.mark.parametrize( + ("model", "field", "max_length"), + [ + (RlsapiV1Attachment, "contents", 65_536), + (RlsapiV1Terminal, "output", 65_536), + (RlsapiV1Context, "stdin", 65_536), + (RlsapiV1InferRequest, "question", 10_240), + ], + ids=[ + "attachment-contents", + "terminal-output", + "context-stdin", + "infer-request-question", + ], +) +def test_value_max_length(model, field, max_length) -> None: + """Test that fields with longer than allowed data are not allowed""" + value = "a" * max_length + bad_value = value + "a" + + instance = model(**{field: value}) + with pytest.raises( + ValidationError, + match=f"should have at most {max_length} characters", + ): + model(**{field: bad_value}) + + assert getattr(instance, field) == value