style: apply ruff's automatic fixes and formatter
Mechanical only, and separated from the judgment calls that follow so the reviewable changes are not buried in a 98-file whitespace diff. 227 automatic fixes: 60 blank lines carrying whitespace, 60 unsorted import blocks, 34 Optional[X] to X | None, 28 unused imports, 16 deprecated typing imports, 12 datetime.timezone.utc to datetime.UTC, and assorted smaller modernisations. Then `ruff format` over src and tests: 98 files reformatted, 35 already conforming. No file among the unused-import findings defines __all__ or is an __init__.py, so nothing here removes a re-export. `make test`: 658 passed, unchanged from HEAD. Two things observed while verifying, neither addressed here: `pytest tests/` cannot collect — tests/e2e/test_orchestration_e2e.py uses an `e2e` marker that is not registered, and the config is strict about markers. This fails identically at HEAD, so it predates this change; `make test` passes because it ignores tests/e2e, tests/integration and tests/contracts. test_tatlock_tool_call_logging_calculator is flaky. It failed once in a full run with these changes and passed on the next, passes in isolation with them, and fails in isolation at HEAD. It is order- or timing-dependent, not a regression from this commit — established by running the full suite both ways rather than by reasoning about which change could have caused it. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,9 @@ Tests:
|
||||
- Stop sequence detection and enforcement
|
||||
- Max tokens enforcement
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
@@ -15,11 +17,11 @@ from pydantic import ValidationError
|
||||
from src.responses.schemas import ResponseRequest
|
||||
from src.responses.streaming import StreamingCoordinator
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Parameter Validation Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_temperature_validation():
|
||||
"""Test temperature parameter validation."""
|
||||
@@ -27,9 +29,7 @@ def test_temperature_validation():
|
||||
valid_temps = [0.0, 0.5, 1.0, 1.5, 2.0]
|
||||
for temp in valid_temps:
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
temperature=temp
|
||||
model="lorem-tester", input=[{"role": "user", "content": "Hello"}], temperature=temp
|
||||
)
|
||||
assert request.temperature == temp
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_temperature_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
temperature=-0.1 # Too low
|
||||
temperature=-0.1, # Too low
|
||||
)
|
||||
assert "temperature" in str(exc_info.value).lower()
|
||||
|
||||
@@ -46,7 +46,7 @@ def test_temperature_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
temperature=2.1 # Too high
|
||||
temperature=2.1, # Too high
|
||||
)
|
||||
assert "temperature" in str(exc_info.value).lower()
|
||||
|
||||
@@ -55,12 +55,12 @@ def test_temperature_validation():
|
||||
def test_reasoning_effort_validation():
|
||||
"""Test reasoning.effort parameter validation."""
|
||||
# Valid effort levels
|
||||
valid_efforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']
|
||||
valid_efforts = ["none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
for effort in valid_efforts:
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
reasoning={"effort": effort, "summary": "auto"}
|
||||
reasoning={"effort": effort, "summary": "auto"},
|
||||
)
|
||||
assert request.reasoning["effort"] == effort
|
||||
|
||||
@@ -69,7 +69,7 @@ def test_reasoning_effort_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
reasoning={"effort": "invalid", "summary": "auto"}
|
||||
reasoning={"effort": "invalid", "summary": "auto"},
|
||||
)
|
||||
assert "reasoning.effort" in str(exc_info.value)
|
||||
|
||||
@@ -78,12 +78,12 @@ def test_reasoning_effort_validation():
|
||||
def test_reasoning_summary_validation():
|
||||
"""Test reasoning.summary parameter validation."""
|
||||
# Valid summary values
|
||||
valid_summaries = ['auto', 'off']
|
||||
valid_summaries = ["auto", "off"]
|
||||
for summary in valid_summaries:
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
reasoning={"effort": "medium", "summary": summary}
|
||||
reasoning={"effort": "medium", "summary": summary},
|
||||
)
|
||||
assert request.reasoning["summary"] == summary
|
||||
|
||||
@@ -92,7 +92,7 @@ def test_reasoning_summary_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
reasoning={"effort": "medium", "summary": "invalid"}
|
||||
reasoning={"effort": "medium", "summary": "invalid"},
|
||||
)
|
||||
assert "reasoning.summary" in str(exc_info.value)
|
||||
|
||||
@@ -102,26 +102,20 @@ def test_max_output_tokens_validation():
|
||||
"""Test max_output_tokens parameter validation."""
|
||||
# Valid values
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
max_output_tokens=100
|
||||
model="lorem-tester", input=[{"role": "user", "content": "Hello"}], max_output_tokens=100
|
||||
)
|
||||
assert request.max_output_tokens == 100
|
||||
|
||||
# None is valid (unlimited)
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
max_output_tokens=None
|
||||
model="lorem-tester", input=[{"role": "user", "content": "Hello"}], max_output_tokens=None
|
||||
)
|
||||
assert request.max_output_tokens is None
|
||||
|
||||
# Invalid: zero or negative
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
max_output_tokens=0
|
||||
model="lorem-tester", input=[{"role": "user", "content": "Hello"}], max_output_tokens=0
|
||||
)
|
||||
assert "max_output_tokens" in str(exc_info.value)
|
||||
|
||||
@@ -129,7 +123,7 @@ def test_max_output_tokens_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
max_output_tokens=-10
|
||||
max_output_tokens=-10,
|
||||
)
|
||||
assert "max_output_tokens" in str(exc_info.value)
|
||||
|
||||
@@ -141,9 +135,7 @@ def test_stop_sequences_validation():
|
||||
for num_seqs in range(1, 5):
|
||||
stop_seqs = [f"stop{i}" for i in range(num_seqs)]
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
stop=stop_seqs
|
||||
model="lorem-tester", input=[{"role": "user", "content": "Hello"}], stop=stop_seqs
|
||||
)
|
||||
assert request.stop == stop_seqs
|
||||
|
||||
@@ -152,7 +144,7 @@ def test_stop_sequences_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
stop=["stop1", "stop2", "stop3", "stop4", "stop5"] # 5 sequences
|
||||
stop=["stop1", "stop2", "stop3", "stop4", "stop5"], # 5 sequences
|
||||
)
|
||||
assert "4 stop sequences" in str(exc_info.value)
|
||||
|
||||
@@ -161,7 +153,7 @@ def test_stop_sequences_validation():
|
||||
ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
stop=["stop1", ""] # Empty string
|
||||
stop=["stop1", ""], # Empty string
|
||||
)
|
||||
assert "non-empty" in str(exc_info.value).lower()
|
||||
|
||||
@@ -170,6 +162,7 @@ def test_stop_sequences_validation():
|
||||
# Stop Sequence Enforcement Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_stop_sequence_detection_helper():
|
||||
"""Test stop sequence detection helper method."""
|
||||
@@ -181,26 +174,17 @@ def test_stop_sequence_detection_helper():
|
||||
assert text == "Hello world"
|
||||
|
||||
# Stop sequence not present
|
||||
found, text = coordinator._check_stop_sequence(
|
||||
"Hello world",
|
||||
["STOP", "END"]
|
||||
)
|
||||
found, text = coordinator._check_stop_sequence("Hello world", ["STOP", "END"])
|
||||
assert found is False
|
||||
assert text == "Hello world"
|
||||
|
||||
# Stop sequence found
|
||||
found, text = coordinator._check_stop_sequence(
|
||||
"Hello STOP this should not appear",
|
||||
["STOP"]
|
||||
)
|
||||
found, text = coordinator._check_stop_sequence("Hello STOP this should not appear", ["STOP"])
|
||||
assert found is True
|
||||
assert text == "Hello "
|
||||
|
||||
# Multiple stop sequences, first one wins
|
||||
found, text = coordinator._check_stop_sequence(
|
||||
"Hello STOP this END that",
|
||||
["STOP", "END"]
|
||||
)
|
||||
found, text = coordinator._check_stop_sequence("Hello STOP this END that", ["STOP", "END"])
|
||||
assert found is True
|
||||
assert text == "Hello "
|
||||
|
||||
@@ -216,7 +200,7 @@ async def test_stop_sequence_in_streaming(async_client: AsyncClient):
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Generate long text"}],
|
||||
"stop": ["dolor"], # Common word in lorem ipsum
|
||||
"stream": True
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
chunks_received = []
|
||||
@@ -251,6 +235,7 @@ async def test_stop_sequence_in_streaming(async_client: AsyncClient):
|
||||
# Max Tokens Enforcement Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_max_tokens_check_helper():
|
||||
"""Test max tokens check helper method."""
|
||||
@@ -296,7 +281,7 @@ async def test_max_tokens_in_streaming(async_client: AsyncClient):
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Generate text"}],
|
||||
"max_output_tokens": 5, # Very low limit
|
||||
"stream": True
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
chunks_received = []
|
||||
@@ -340,6 +325,7 @@ async def test_max_tokens_in_streaming(async_client: AsyncClient):
|
||||
# Combined Features Test
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_combined_validation(client: TestClient):
|
||||
"""Test combined parameter validation in actual request."""
|
||||
@@ -351,7 +337,7 @@ def test_combined_validation(client: TestClient):
|
||||
"max_output_tokens": 100,
|
||||
"stop": ["STOP", "END"],
|
||||
"reasoning": {"effort": "high", "summary": "auto"},
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -368,7 +354,7 @@ def test_invalid_combined_parameters(client: TestClient):
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"temperature": 3.0, # Too high
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -382,6 +368,7 @@ def test_invalid_combined_parameters(client: TestClient):
|
||||
# Streaming Delta Calculation Tests (No Duplication)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_delta_calculation_no_duplication():
|
||||
@@ -392,8 +379,10 @@ async def test_streaming_delta_calculation_no_duplication():
|
||||
This test prevents the duplication bug where the same text was
|
||||
streamed multiple times because we weren't computing deltas correctly.
|
||||
"""
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from src.agents.base import AgentInterface, OutputItem
|
||||
from typing import AsyncGenerator, Any
|
||||
|
||||
# Create a mock agent that simulates PydanticAI's behavior
|
||||
# (yielding accumulated text, not deltas)
|
||||
@@ -406,7 +395,7 @@ async def test_streaming_delta_calculation_no_duplication():
|
||||
temperature: float = 1.0,
|
||||
max_tokens: int | None = None,
|
||||
stop: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
**kwargs: Any,
|
||||
) -> AsyncGenerator[OutputItem, None]:
|
||||
"""
|
||||
Simulate PydanticAI streaming behavior:
|
||||
@@ -430,12 +419,8 @@ async def test_streaming_delta_calculation_no_duplication():
|
||||
type="message",
|
||||
id=msg_id,
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": text,
|
||||
"annotations": []
|
||||
}],
|
||||
status="in_progress"
|
||||
content=[{"type": "output_text", "text": text, "annotations": []}],
|
||||
status="in_progress",
|
||||
)
|
||||
|
||||
# Final message
|
||||
@@ -443,12 +428,10 @@ async def test_streaming_delta_calculation_no_duplication():
|
||||
type="message",
|
||||
id=msg_id,
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": "Hello world how are you",
|
||||
"annotations": []
|
||||
}],
|
||||
status="completed"
|
||||
content=[
|
||||
{"type": "output_text", "text": "Hello world how are you", "annotations": []}
|
||||
],
|
||||
status="completed",
|
||||
)
|
||||
|
||||
async def supports_tools(self) -> bool:
|
||||
@@ -462,7 +445,9 @@ async def test_streaming_delta_calculation_no_duplication():
|
||||
|
||||
# Register the mock agent
|
||||
import time
|
||||
|
||||
from src.agents.registry import ModelRegistry
|
||||
|
||||
ModelRegistry.MODELS["mock-streaming"] = {
|
||||
"agent_class": MockStreamingAgent,
|
||||
"description": "Mock streaming agent for testing",
|
||||
@@ -473,9 +458,7 @@ async def test_streaming_delta_calculation_no_duplication():
|
||||
try:
|
||||
# Create a test request
|
||||
request = ResponseRequest(
|
||||
model="mock-streaming",
|
||||
input=[{"role": "user", "content": "Test"}],
|
||||
stream=True
|
||||
model="mock-streaming", input=[{"role": "user", "content": "Test"}], stream=True
|
||||
)
|
||||
|
||||
# Stream the response
|
||||
@@ -512,8 +495,10 @@ async def test_streaming_with_multiple_message_items():
|
||||
Test that coordinator handles multiple message OutputItems correctly,
|
||||
only streaming the delta between each one.
|
||||
"""
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from src.agents.base import AgentInterface, OutputItem
|
||||
from typing import AsyncGenerator, Any
|
||||
|
||||
class MockMultiMessageAgent(AgentInterface):
|
||||
async def generate_response(
|
||||
@@ -524,7 +509,7 @@ async def test_streaming_with_multiple_message_items():
|
||||
temperature: float = 1.0,
|
||||
max_tokens: int | None = None,
|
||||
stop: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
**kwargs: Any,
|
||||
) -> AsyncGenerator[OutputItem, None]:
|
||||
"""Yield multiple in_progress messages with accumulated text."""
|
||||
# First chunk
|
||||
@@ -533,7 +518,7 @@ async def test_streaming_with_multiple_message_items():
|
||||
id="msg_1",
|
||||
role="assistant",
|
||||
content=[{"type": "output_text", "text": "The answer is", "annotations": []}],
|
||||
status="in_progress"
|
||||
status="in_progress",
|
||||
)
|
||||
|
||||
# Second chunk (more text accumulated)
|
||||
@@ -542,7 +527,7 @@ async def test_streaming_with_multiple_message_items():
|
||||
id="msg_1",
|
||||
role="assistant",
|
||||
content=[{"type": "output_text", "text": "The answer is 42", "annotations": []}],
|
||||
status="in_progress"
|
||||
status="in_progress",
|
||||
)
|
||||
|
||||
# Final chunk
|
||||
@@ -551,7 +536,7 @@ async def test_streaming_with_multiple_message_items():
|
||||
id="msg_1",
|
||||
role="assistant",
|
||||
content=[{"type": "output_text", "text": "The answer is 42", "annotations": []}],
|
||||
status="completed"
|
||||
status="completed",
|
||||
)
|
||||
|
||||
async def supports_tools(self) -> bool:
|
||||
@@ -565,7 +550,9 @@ async def test_streaming_with_multiple_message_items():
|
||||
|
||||
# Register mock agent
|
||||
import time
|
||||
|
||||
from src.agents.registry import ModelRegistry
|
||||
|
||||
ModelRegistry.MODELS["mock-multi"] = {
|
||||
"agent_class": MockMultiMessageAgent,
|
||||
"description": "Mock multi-message agent for testing",
|
||||
@@ -577,7 +564,7 @@ async def test_streaming_with_multiple_message_items():
|
||||
request = ResponseRequest(
|
||||
model="mock-multi",
|
||||
input=[{"role": "user", "content": "What is the answer?"}],
|
||||
stream=True
|
||||
stream=True,
|
||||
)
|
||||
|
||||
coordinator = StreamingCoordinator()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
Tests for error handling in Responses API.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
@@ -12,10 +14,8 @@ def test_model_not_found_error(client: TestClient) -> None:
|
||||
"""Test response when model doesn't exist."""
|
||||
request_data = {
|
||||
"model": "nonexistent-model-12345",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -29,11 +29,7 @@ def test_model_not_found_error(client: TestClient) -> None:
|
||||
@pytest.mark.unit
|
||||
def test_validation_error_missing_model(client: TestClient) -> None:
|
||||
"""Test validation error when model field is missing."""
|
||||
request_data = {
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}
|
||||
request_data = {"input": [{"role": "user", "content": "Hello"}]}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
|
||||
@@ -45,10 +41,8 @@ def test_validation_error_invalid_temperature(client: TestClient) -> None:
|
||||
"""Test validation error for out-of-range temperature."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"temperature": 3.0 # Max is 2.0
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"temperature": 3.0, # Max is 2.0
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -61,10 +55,8 @@ def test_rate_limit_error(client: TestClient) -> None:
|
||||
"""Test rate limit error trigger."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "trigger_rate_limit"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "trigger_rate_limit"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -78,10 +70,8 @@ def test_context_overflow_error(client: TestClient) -> None:
|
||||
"""Test context length overflow error trigger."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "trigger_context_overflow"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "trigger_context_overflow"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -96,10 +86,8 @@ async def test_streaming_model_not_found(async_client: AsyncClient) -> None:
|
||||
"""Test streaming response with nonexistent model."""
|
||||
request_data = {
|
||||
"model": "nonexistent-streaming-model",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": True
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with async_client.stream(
|
||||
@@ -133,10 +121,8 @@ async def test_streaming_rate_limit_error(async_client: AsyncClient) -> None:
|
||||
"""Test streaming with rate limit error."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "trigger_rate_limit"}
|
||||
],
|
||||
"stream": True
|
||||
"input": [{"role": "user", "content": "trigger_rate_limit"}],
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with async_client.stream(
|
||||
@@ -177,10 +163,8 @@ async def test_streaming_context_overflow_error(async_client: AsyncClient) -> No
|
||||
"""Test streaming with context overflow error."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "trigger_context_overflow"}
|
||||
],
|
||||
"stream": True
|
||||
"input": [{"role": "user", "content": "trigger_context_overflow"}],
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with async_client.stream(
|
||||
@@ -215,17 +199,9 @@ def test_function_call_output_item(client: TestClient) -> None:
|
||||
"""Test response with function call items."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Use the search tool"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Search for information",
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Use the search tool"}],
|
||||
"tools": [{"name": "search", "description": "Search for information", "parameters": {}}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -248,10 +224,8 @@ def test_pipeline_prefix_stripping(client: TestClient) -> None:
|
||||
"""Test that pipeline prefixes are stripped from model names."""
|
||||
request_data = {
|
||||
"model": "some_pipeline.lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -267,10 +241,8 @@ def test_multiple_pipeline_prefixes(client: TestClient) -> None:
|
||||
"""Test multiple dots in model name (only first is prefix)."""
|
||||
request_data = {
|
||||
"model": "pipeline.sub.lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""
|
||||
Tests for conversation history management.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.responses.history import ConversationHistory
|
||||
from src.responses.context import ContextWindow
|
||||
from src.responses.schemas import ResponseRequest
|
||||
from src.responses import service
|
||||
from src.responses.context import ContextWindow
|
||||
from src.responses.history import ConversationHistory
|
||||
from src.responses.schemas import ResponseRequest
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -19,7 +20,7 @@ async def test_conversation_id_from_metadata():
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
metadata={"conversation_id": "conv_123"}
|
||||
metadata={"conversation_id": "conv_123"},
|
||||
)
|
||||
|
||||
conv_id = await history.get_conversation_id(request)
|
||||
@@ -34,7 +35,7 @@ async def test_conversation_id_generation():
|
||||
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}]
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
# No metadata provided
|
||||
)
|
||||
|
||||
@@ -43,10 +44,7 @@ async def test_conversation_id_generation():
|
||||
assert len(conv_id) == 16 # 16 character hex
|
||||
|
||||
# Same first message should generate same ID
|
||||
request2 = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
request2 = ResponseRequest(model="lorem-tester", input=[{"role": "user", "content": "Hello"}])
|
||||
conv_id2 = await history.get_conversation_id(request2)
|
||||
assert conv_id == conv_id2
|
||||
|
||||
@@ -59,7 +57,7 @@ def test_conversation_history_tracking(client: TestClient):
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"metadata": {"conversation_id": "test_conv_001"},
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -71,10 +69,10 @@ def test_conversation_history_tracking(client: TestClient):
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there!"},
|
||||
{"role": "user", "content": "How are you?"}
|
||||
{"role": "user", "content": "How are you?"},
|
||||
],
|
||||
"metadata": {"conversation_id": "test_conv_001"},
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response2 = client.post("/v1/responses", json=request_data2)
|
||||
@@ -94,7 +92,7 @@ async def test_conversation_history_retrieval():
|
||||
request = ResponseRequest(
|
||||
model="lorem-tester",
|
||||
input=[{"role": "user", "content": "Test"}],
|
||||
metadata={"conversation_id": "test_retrieve"}
|
||||
metadata={"conversation_id": "test_retrieve"},
|
||||
)
|
||||
|
||||
conv_id = await history.get_conversation_id(request)
|
||||
@@ -104,24 +102,15 @@ async def test_conversation_history_retrieval():
|
||||
assert len(retrieved) == 0
|
||||
|
||||
# Add mock response
|
||||
from src.responses.schemas import Response, ResponseUsage, MessageOutputItem, OutputTextContent
|
||||
from src.responses.schemas import MessageOutputItem, OutputTextContent, Response, ResponseUsage
|
||||
|
||||
mock_response = Response(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
model="lorem-tester",
|
||||
status="completed",
|
||||
output=[
|
||||
MessageOutputItem(
|
||||
id="msg_1",
|
||||
content=[OutputTextContent(text="Test response")]
|
||||
)
|
||||
],
|
||||
usage=ResponseUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=5,
|
||||
reasoning_tokens=0,
|
||||
total_tokens=15
|
||||
)
|
||||
output=[MessageOutputItem(id="msg_1", content=[OutputTextContent(text="Test response")])],
|
||||
usage=ResponseUsage(input_tokens=10, output_tokens=5, reasoning_tokens=0, total_tokens=15),
|
||||
)
|
||||
|
||||
await history.add_response(conv_id, mock_response)
|
||||
@@ -138,7 +127,7 @@ async def test_conversation_history_trimming():
|
||||
"""Test that history is trimmed to max_turns."""
|
||||
history = ConversationHistory(max_turns=3)
|
||||
|
||||
from src.responses.schemas import Response, ResponseUsage, MessageOutputItem, OutputTextContent
|
||||
from src.responses.schemas import MessageOutputItem, OutputTextContent, Response, ResponseUsage
|
||||
|
||||
conv_id = "test_trim"
|
||||
|
||||
@@ -150,17 +139,11 @@ async def test_conversation_history_trimming():
|
||||
model="lorem-tester",
|
||||
status="completed",
|
||||
output=[
|
||||
MessageOutputItem(
|
||||
id=f"msg_{i}",
|
||||
content=[OutputTextContent(text=f"Response {i}")]
|
||||
)
|
||||
MessageOutputItem(id=f"msg_{i}", content=[OutputTextContent(text=f"Response {i}")])
|
||||
],
|
||||
usage=ResponseUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=5,
|
||||
reasoning_tokens=0,
|
||||
total_tokens=15
|
||||
)
|
||||
input_tokens=10, output_tokens=5, reasoning_tokens=0, total_tokens=15
|
||||
),
|
||||
)
|
||||
await history.add_response(conv_id, response)
|
||||
|
||||
@@ -178,7 +161,7 @@ async def test_clear_conversation():
|
||||
"""Test clearing conversation history."""
|
||||
history = ConversationHistory()
|
||||
|
||||
from src.responses.schemas import Response, ResponseUsage, MessageOutputItem, OutputTextContent
|
||||
from src.responses.schemas import MessageOutputItem, OutputTextContent, Response, ResponseUsage
|
||||
|
||||
conv_id = "test_clear"
|
||||
|
||||
@@ -188,18 +171,8 @@ async def test_clear_conversation():
|
||||
created_at=1234567890,
|
||||
model="lorem-tester",
|
||||
status="completed",
|
||||
output=[
|
||||
MessageOutputItem(
|
||||
id="msg_1",
|
||||
content=[OutputTextContent(text="Test")]
|
||||
)
|
||||
],
|
||||
usage=ResponseUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=5,
|
||||
reasoning_tokens=0,
|
||||
total_tokens=15
|
||||
)
|
||||
output=[MessageOutputItem(id="msg_1", content=[OutputTextContent(text="Test")])],
|
||||
usage=ResponseUsage(input_tokens=10, output_tokens=5, reasoning_tokens=0, total_tokens=15),
|
||||
)
|
||||
await history.add_response(conv_id, response)
|
||||
|
||||
@@ -227,7 +200,7 @@ async def test_conversation_count():
|
||||
initial_count = await history.get_conversation_count()
|
||||
|
||||
# Add responses to 3 different conversations
|
||||
from src.responses.schemas import Response, ResponseUsage, MessageOutputItem, OutputTextContent
|
||||
from src.responses.schemas import MessageOutputItem, OutputTextContent, Response, ResponseUsage
|
||||
|
||||
for i in range(3):
|
||||
response = Response(
|
||||
@@ -235,18 +208,10 @@ async def test_conversation_count():
|
||||
created_at=1234567890,
|
||||
model="lorem-tester",
|
||||
status="completed",
|
||||
output=[
|
||||
MessageOutputItem(
|
||||
id=f"msg_{i}",
|
||||
content=[OutputTextContent(text="Test")]
|
||||
)
|
||||
],
|
||||
output=[MessageOutputItem(id=f"msg_{i}", content=[OutputTextContent(text="Test")])],
|
||||
usage=ResponseUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=5,
|
||||
reasoning_tokens=0,
|
||||
total_tokens=15
|
||||
)
|
||||
input_tokens=10, output_tokens=5, reasoning_tokens=0, total_tokens=15
|
||||
),
|
||||
)
|
||||
await history.add_response(f"conv_{i}", response)
|
||||
|
||||
@@ -281,8 +246,8 @@ async def test_context_window_trimming():
|
||||
# Create items that exceed limit
|
||||
items = [
|
||||
"This is a long message " * 20, # ~480 chars = ~120 tokens
|
||||
"Another message " * 10, # ~160 chars = ~40 tokens
|
||||
"Short message" # ~13 chars = ~3 tokens
|
||||
"Another message " * 10, # ~160 chars = ~40 tokens
|
||||
"Short message", # ~13 chars = ~3 tokens
|
||||
]
|
||||
|
||||
# Trim with 10 token reserve
|
||||
@@ -355,14 +320,12 @@ async def test_service_conversation_helpers():
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_tracks_history(async_client):
|
||||
"""Test that streaming responses also track conversation history."""
|
||||
from httpx import AsyncClient
|
||||
import json
|
||||
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"metadata": {"conversation_id": "stream_test_001"},
|
||||
"stream": True
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with async_client.stream(
|
||||
|
||||
@@ -3,6 +3,7 @@ Tests for Responses API router.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
@@ -13,10 +14,8 @@ def test_create_response_non_streaming(client: TestClient) -> None:
|
||||
"""Test non-streaming response creation."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Hello, how are you?"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -48,14 +47,9 @@ def test_create_response_with_reasoning(client: TestClient) -> None:
|
||||
"""Test response with reasoning enabled."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Explain something"}
|
||||
],
|
||||
"reasoning": {
|
||||
"effort": "medium",
|
||||
"summary": "auto"
|
||||
},
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Explain something"}],
|
||||
"reasoning": {"effort": "medium", "summary": "auto"},
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -88,17 +82,11 @@ def test_create_response_with_tools(client: TestClient) -> None:
|
||||
"""Test response with tools available."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Use a tool to help"}
|
||||
],
|
||||
"input": [{"role": "user", "content": "Use a tool to help"}],
|
||||
"tools": [
|
||||
{
|
||||
"name": "search_knowledge",
|
||||
"description": "Search knowledge base",
|
||||
"parameters": {}
|
||||
}
|
||||
{"name": "search_knowledge", "description": "Search knowledge base", "parameters": {}}
|
||||
],
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -117,10 +105,8 @@ def test_create_response_invalid_model(client: TestClient) -> None:
|
||||
"""Test response with non-existent model."""
|
||||
request_data = {
|
||||
"model": "nonexistent-model",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": False
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -134,11 +120,7 @@ def test_create_response_invalid_model(client: TestClient) -> None:
|
||||
def test_create_response_validation_error(client: TestClient) -> None:
|
||||
"""Test response with invalid request data."""
|
||||
# Missing required 'model' field
|
||||
request_data = {
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}
|
||||
request_data = {"input": [{"role": "user", "content": "Hello"}]}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
|
||||
@@ -151,10 +133,8 @@ async def test_create_response_streaming(async_client: AsyncClient) -> None:
|
||||
"""Test streaming response creation."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": True
|
||||
"input": [{"role": "user", "content": "Hello"}],
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with async_client.stream(
|
||||
@@ -200,14 +180,9 @@ async def test_create_response_streaming_with_reasoning(async_client: AsyncClien
|
||||
"""Test streaming response with reasoning."""
|
||||
request_data = {
|
||||
"model": "lorem-tester",
|
||||
"input": [
|
||||
{"role": "user", "content": "Explain"}
|
||||
],
|
||||
"reasoning": {
|
||||
"effort": "medium",
|
||||
"summary": "auto"
|
||||
},
|
||||
"stream": True
|
||||
"input": [{"role": "user", "content": "Explain"}],
|
||||
"reasoning": {"effort": "medium", "summary": "auto"},
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with async_client.stream(
|
||||
@@ -238,16 +213,12 @@ async def test_create_response_streaming_with_reasoning(async_client: AsyncClien
|
||||
|
||||
# Should have reasoning events
|
||||
reasoning_events = [
|
||||
e for e in events
|
||||
if e["event"] == "response.reasoning_summary_text.delta"
|
||||
e for e in events if e["event"] == "response.reasoning_summary_text.delta"
|
||||
]
|
||||
assert len(reasoning_events) > 0
|
||||
|
||||
# Should have output text events
|
||||
output_events = [
|
||||
e for e in events
|
||||
if e["event"] == "response.output_text.delta"
|
||||
]
|
||||
output_events = [e for e in events if e["event"] == "response.output_text.delta"]
|
||||
assert len(output_events) > 0
|
||||
|
||||
|
||||
@@ -258,7 +229,7 @@ def test_create_response_temperature_parameter(client: TestClient) -> None:
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Test"}],
|
||||
"temperature": 0.5,
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
@@ -273,7 +244,7 @@ def test_create_response_max_tokens_parameter(client: TestClient) -> None:
|
||||
"model": "lorem-tester",
|
||||
"input": [{"role": "user", "content": "Test"}],
|
||||
"max_output_tokens": 100,
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = client.post("/v1/responses", json=request_data)
|
||||
|
||||
@@ -53,9 +53,7 @@ class TestStreamDirectDelegation:
|
||||
) as mock_delegate:
|
||||
gen = coordinator._stream_direct_delegation(
|
||||
user_message="Search for Docker info",
|
||||
recommendation=SimpleNamespace(
|
||||
recommended_capabilities=["librarian"]
|
||||
),
|
||||
recommendation=SimpleNamespace(recommended_capabilities=["librarian"]),
|
||||
tracker=tracker,
|
||||
conversation_id="conv_1",
|
||||
conversation_history=HISTORY,
|
||||
@@ -66,9 +64,9 @@ class TestStreamDirectDelegation:
|
||||
first = await gen.__anext__()
|
||||
assert isinstance(first, ReasoningSummaryDelta)
|
||||
assert first.delta.strip() != ""
|
||||
assert mock_delegate.await_count == 0, (
|
||||
"start think message must stream before the expert runs"
|
||||
)
|
||||
assert (
|
||||
mock_delegate.await_count == 0
|
||||
), "start think message must stream before the expert runs"
|
||||
|
||||
second = await gen.__anext__()
|
||||
assert isinstance(second, ReasoningSummaryDone)
|
||||
@@ -101,9 +99,7 @@ class TestStreamDirectDelegation:
|
||||
event
|
||||
async for event in coordinator._stream_direct_delegation(
|
||||
user_message="And what services does it list?",
|
||||
recommendation=SimpleNamespace(
|
||||
recommended_capabilities=["librarian"]
|
||||
),
|
||||
recommendation=SimpleNamespace(recommended_capabilities=["librarian"]),
|
||||
tracker=tracker,
|
||||
conversation_id="conv_1",
|
||||
conversation_history=HISTORY,
|
||||
@@ -139,9 +135,7 @@ class TestStreamDirectDelegation:
|
||||
event
|
||||
async for event in coordinator._stream_direct_delegation(
|
||||
user_message="Search for Docker info",
|
||||
recommendation=SimpleNamespace(
|
||||
recommended_capabilities=["librarian"]
|
||||
),
|
||||
recommendation=SimpleNamespace(recommended_capabilities=["librarian"]),
|
||||
tracker=tracker,
|
||||
conversation_id="conv_1",
|
||||
results=results,
|
||||
@@ -172,9 +166,7 @@ class TestServiceDelegationContext:
|
||||
) as mock_delegate:
|
||||
results = await _direct_delegation_with_results(
|
||||
user_message="And what services does it list?",
|
||||
recommendation=SimpleNamespace(
|
||||
recommended_capabilities=["librarian"]
|
||||
),
|
||||
recommendation=SimpleNamespace(recommended_capabilities=["librarian"]),
|
||||
tracker=tracker,
|
||||
conversation_id="conv_1",
|
||||
conversation_history=HISTORY,
|
||||
|
||||
Reference in New Issue
Block a user