llama-stack-mirror/tests/integration/responses/test_conversation_responses.py
Sébastien Han d82a2cd6f8
Some checks failed
SqlStore Integration Tests / test-postgres (3.12) (push) Failing after 0s
Integration Auth Tests / test-matrix (oauth2_token) (push) Failing after 1s
Test External Providers Installed via Module / test-external-providers-from-module (venv) (push) Has been skipped
SqlStore Integration Tests / test-postgres (3.13) (push) Failing after 4s
Integration Tests (Replay) / generate-matrix (push) Successful in 3s
API Conformance Tests / check-schema-compatibility (push) Successful in 10s
Python Package Build Test / build (3.13) (push) Successful in 17s
Python Package Build Test / build (3.12) (push) Successful in 18s
Test External API and Providers / test-external (venv) (push) Failing after 21s
Vector IO Integration Tests / test-matrix (push) Failing after 33s
UI Tests / ui-tests (22) (push) Successful in 1m13s
Unit Tests / unit-tests (3.12) (push) Failing after 1m37s
Unit Tests / unit-tests (3.13) (push) Failing after 2m11s
Pre-commit / pre-commit (22) (push) Successful in 3m39s
Integration Tests (Replay) / Integration Tests (, , , client=, ) (push) Failing after 4m1s
fix: httpcore deadlock in CI by properly closing streaming responses (#4335)
# What does this PR do?

The test_conversation_error_handling test was timing out in CI with a
deadlock in httpcore's connection pool. The root cause was the preceding
test_conversation_multi_turn_and_streaming test, which broke out of the
streaming response iterator early without properly closing the
underlying HTTP connection.

When a streaming response iterator is abandoned mid-stream, the HTTP
connection remains in an incomplete state. Since the openai_client
fixture is session-scoped, subsequent tests reuse the same httpcore
connection pool. The dangling connection causes the pool's internal lock
to deadlock when the next test attempts to acquire a new connection.

The fix wraps the streaming response in a context manager, which ensures
the connection is properly closed when exiting the with block, even when
breaking out of the loop early. This is a best practice when working
with streaming HTTP responses that may not be fully consumed.

Signed-off-by: Sébastien Han <seb@redhat.com>
2025-12-08 16:38:46 +01:00

143 lines
6.1 KiB
Python

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
import pytest
@pytest.mark.integration
class TestConversationResponses:
"""Integration tests for the conversation parameter in responses API."""
def test_conversation_basic_workflow(self, openai_client, text_model_id):
"""Test basic conversation workflow: create conversation, add response, verify sync."""
conversation = openai_client.conversations.create(metadata={"topic": "test"})
assert conversation.id.startswith("conv_")
response = openai_client.responses.create(
model=text_model_id,
input=[{"role": "user", "content": "What are the 5 Ds of dodgeball?"}],
conversation=conversation.id,
)
assert response.id.startswith("resp_")
assert len(response.output_text.strip()) > 0
# Verify conversation was synced properly
conversation_items = openai_client.conversations.items.list(conversation.id)
assert len(conversation_items.data) >= 2
roles = [item.role for item in conversation_items.data if hasattr(item, "role")]
assert "user" in roles and "assistant" in roles
def test_conversation_multi_turn_and_streaming(self, openai_client, text_model_id):
"""Test multi-turn conversations and streaming responses."""
conversation = openai_client.conversations.create()
# First turn
response1 = openai_client.responses.create(
model=text_model_id,
input=[{"role": "user", "content": "Say hello"}],
conversation=conversation.id,
)
# Second turn with streaming - use context manager to ensure proper connection cleanup
with openai_client.responses.create(
model=text_model_id,
input=[{"role": "user", "content": "Say goodbye"}],
conversation=conversation.id,
stream=True,
) as response_stream:
final_response = None
for chunk in response_stream:
if chunk.type == "response.completed":
final_response = chunk.response
break
assert response1.id != final_response.id
assert len(response1.output_text.strip()) > 0
assert len(final_response.output_text.strip()) > 0
# Verify all turns are in conversation
conversation_items = openai_client.conversations.items.list(conversation.id)
assert len(conversation_items.data) >= 4 # 2 user + 2 assistant messages
@pytest.mark.timeout(60, method="thread")
def test_conversation_context_loading(self, openai_client, text_model_id):
"""Test that conversation context is properly loaded for responses."""
conversation = openai_client.conversations.create(
items=[
{"type": "message", "role": "user", "content": "My name is Alice. I like to eat apples."},
{"type": "message", "role": "assistant", "content": "Hello Alice!"},
]
)
response = openai_client.responses.create(
model=text_model_id,
input=[{"role": "user", "content": "What do I like to eat?"}],
conversation=conversation.id,
)
assert "apple" in response.output_text.lower()
@pytest.mark.timeout(60, method="thread")
def test_conversation_error_handling(self, openai_client, text_model_id):
"""Test error handling for invalid and nonexistent conversations."""
# Invalid conversation ID format
with pytest.raises(Exception) as exc_info:
openai_client.responses.create(
model=text_model_id,
input=[{"role": "user", "content": "Hello"}],
conversation="invalid_id",
)
assert any(word in str(exc_info.value).lower() for word in ["conv", "invalid", "bad"])
# Nonexistent conversation ID
with pytest.raises(Exception) as exc_info:
openai_client.responses.create(
model=text_model_id,
input=[{"role": "user", "content": "Hello"}],
conversation="conv_nonexistent123",
)
assert any(word in str(exc_info.value).lower() for word in ["not found", "404"])
#
# response = openai_client.responses.create(
# model=text_model_id, input=[{"role": "user", "content": "First response"}]
# )
# with pytest.raises(Exception) as exc_info:
# openai_client.responses.create(
# model=text_model_id,
# input=[{"role": "user", "content": "Hello"}],
# conversation="conv_test123",
# previous_response_id=response.id,
# )
# assert "mutually exclusive" in str(exc_info.value).lower()
def test_conversation_backward_compatibility(self, openai_client, text_model_id):
"""Test that responses work without conversation parameter (backward compatibility)."""
response = openai_client.responses.create(
model=text_model_id, input=[{"role": "user", "content": "Hello world"}]
)
assert response.id.startswith("resp_")
assert len(response.output_text.strip()) > 0
# this is not ready yet
# def test_conversation_compat_client(self, responses_client, text_model_id):
# """Test conversation parameter works with compatibility client."""
# if not hasattr(responses_client, "conversations"):
# pytest.skip("responses_client does not support conversations API")
#
# conversation = responses_client.conversations.create()
# response = responses_client.responses.create(
# model=text_model_id, input="Tell me a joke", conversation=conversation.id
# )
#
# assert response is not None
# assert len(response.output_text.strip()) > 0
#
# conversation_items = responses_client.conversations.items.list(conversation.id)
# assert len(conversation_items.data) >= 2