test: update test suite for PydanticAI integration
- Update conftest for lazy agent initialization - Update chat router tests for Tatlock capabilities - Update models router tests for tools capability - Update responses advanced features tests - Update main app tests - Total: 131 tests, 81.78% coverage (up from 95 tests, 78.95%)
This commit is contained in:
@@ -376,3 +376,226 @@ def test_invalid_combined_parameters(client: TestClient):
|
||||
data = response.json()
|
||||
assert "error" in data
|
||||
assert data["error"]["type"] == "invalid_request_error"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Streaming Delta Calculation Tests (No Duplication)
|
||||
# ============================================================================
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_delta_calculation_no_duplication():
|
||||
"""
|
||||
Test that StreamingCoordinator correctly calculates deltas when agent
|
||||
yields accumulated text multiple times (PydanticAI pattern).
|
||||
|
||||
This test prevents the duplication bug where the same text was
|
||||
streamed multiple times because we weren't computing deltas correctly.
|
||||
"""
|
||||
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)
|
||||
class MockStreamingAgent(AgentInterface):
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: list[dict],
|
||||
reasoning: dict | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
temperature: float = 1.0,
|
||||
max_tokens: int | None = None,
|
||||
stop: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
) -> AsyncGenerator[OutputItem, None]:
|
||||
"""
|
||||
Simulate PydanticAI streaming behavior:
|
||||
- Yields accumulated text, not deltas
|
||||
- Multiple yields with status="in_progress"
|
||||
- Final yield with status="completed"
|
||||
"""
|
||||
msg_id = "msg_test_123"
|
||||
|
||||
# Simulate incremental accumulation like PydanticAI does
|
||||
accumulated_texts = [
|
||||
"Hello",
|
||||
"Hello world",
|
||||
"Hello world how",
|
||||
"Hello world how are",
|
||||
"Hello world how are you",
|
||||
]
|
||||
|
||||
for text in accumulated_texts:
|
||||
yield OutputItem(
|
||||
type="message",
|
||||
id=msg_id,
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": text,
|
||||
"annotations": []
|
||||
}],
|
||||
status="in_progress"
|
||||
)
|
||||
|
||||
# Final message
|
||||
yield OutputItem(
|
||||
type="message",
|
||||
id=msg_id,
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": "Hello world how are you",
|
||||
"annotations": []
|
||||
}],
|
||||
status="completed"
|
||||
)
|
||||
|
||||
async def supports_tools(self) -> bool:
|
||||
return False
|
||||
|
||||
async def supports_reasoning(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_capabilities(self) -> dict:
|
||||
return {"streaming": True, "reasoning": False, "tools": False}
|
||||
|
||||
# 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",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "test",
|
||||
}
|
||||
|
||||
try:
|
||||
# Create a test request
|
||||
request = ResponseRequest(
|
||||
model="mock-streaming",
|
||||
input=[{"role": "user", "content": "Test"}],
|
||||
stream=True
|
||||
)
|
||||
|
||||
# Stream the response
|
||||
coordinator = StreamingCoordinator()
|
||||
collected_deltas = []
|
||||
|
||||
async for event in coordinator.stream_response(request):
|
||||
if event.event == "response.output_text.delta":
|
||||
collected_deltas.append(event.delta)
|
||||
|
||||
# Reconstruct the full text from deltas
|
||||
full_text = "".join(collected_deltas)
|
||||
|
||||
# Verify no duplication - the text should appear exactly once
|
||||
assert full_text.count("Hello") == 1, "Text 'Hello' should appear exactly once"
|
||||
assert full_text.count("world") == 1, "Text 'world' should appear exactly once"
|
||||
assert full_text.count("how") == 1, "Text 'how' should appear exactly once"
|
||||
assert full_text.count("are") == 1, "Text 'are' should appear exactly once"
|
||||
assert full_text.count("you") == 1, "Text 'you' should appear exactly once"
|
||||
|
||||
# Verify the reconstructed text is correct (no trailing space with chunk streaming)
|
||||
expected_text = "Hello world how are you"
|
||||
assert full_text == expected_text, f"Expected '{expected_text}', got '{full_text}'"
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
del ModelRegistry.MODELS["mock-streaming"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_with_multiple_message_items():
|
||||
"""
|
||||
Test that coordinator handles multiple message OutputItems correctly,
|
||||
only streaming the delta between each one.
|
||||
"""
|
||||
from src.agents.base import AgentInterface, OutputItem
|
||||
from typing import AsyncGenerator, Any
|
||||
|
||||
class MockMultiMessageAgent(AgentInterface):
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: list[dict],
|
||||
reasoning: dict | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
temperature: float = 1.0,
|
||||
max_tokens: int | None = None,
|
||||
stop: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
) -> AsyncGenerator[OutputItem, None]:
|
||||
"""Yield multiple in_progress messages with accumulated text."""
|
||||
# First chunk
|
||||
yield OutputItem(
|
||||
type="message",
|
||||
id="msg_1",
|
||||
role="assistant",
|
||||
content=[{"type": "output_text", "text": "The answer is", "annotations": []}],
|
||||
status="in_progress"
|
||||
)
|
||||
|
||||
# Second chunk (more text accumulated)
|
||||
yield OutputItem(
|
||||
type="message",
|
||||
id="msg_1",
|
||||
role="assistant",
|
||||
content=[{"type": "output_text", "text": "The answer is 42", "annotations": []}],
|
||||
status="in_progress"
|
||||
)
|
||||
|
||||
# Final chunk
|
||||
yield OutputItem(
|
||||
type="message",
|
||||
id="msg_1",
|
||||
role="assistant",
|
||||
content=[{"type": "output_text", "text": "The answer is 42", "annotations": []}],
|
||||
status="completed"
|
||||
)
|
||||
|
||||
async def supports_tools(self) -> bool:
|
||||
return False
|
||||
|
||||
async def supports_reasoning(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_capabilities(self) -> dict:
|
||||
return {"streaming": True, "reasoning": False, "tools": False}
|
||||
|
||||
# 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",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "test",
|
||||
}
|
||||
|
||||
try:
|
||||
request = ResponseRequest(
|
||||
model="mock-multi",
|
||||
input=[{"role": "user", "content": "What is the answer?"}],
|
||||
stream=True
|
||||
)
|
||||
|
||||
coordinator = StreamingCoordinator()
|
||||
collected_deltas = []
|
||||
|
||||
async for event in coordinator.stream_response(request):
|
||||
if event.event == "response.output_text.delta":
|
||||
collected_deltas.append(event.delta)
|
||||
|
||||
full_text = "".join(collected_deltas)
|
||||
|
||||
# Should only see "The answer is 42" once, not repeated
|
||||
assert "The answer is 42" in full_text
|
||||
# Count occurrences - should only appear once
|
||||
assert full_text.count("The") == 1
|
||||
assert full_text.count("answer") == 1
|
||||
assert full_text.count("42") == 1
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
del ModelRegistry.MODELS["mock-multi"]
|
||||
|
||||
Reference in New Issue
Block a user