Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/models/rlsapi/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
Expand All @@ -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"],
)
Expand Down Expand Up @@ -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"],
)
Expand Down Expand Up @@ -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?"],
)
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/endpoints/test_rlsapi_v1_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
16 changes: 0 additions & 16 deletions tests/unit/app/endpoints/test_rlsapi_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/models/rlsapi/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading