(pydantic-ai) update tests after last update

There is a new consistendy enforcement in the library, which
enforces each message to have a unique ID, therefor the UUID
mock fails (which was expected TBH).
This commit is contained in:
Quentin BEY
2025-10-27 21:45:12 +01:00
parent 0cce897c69
commit 34cf348f4c
9 changed files with 333 additions and 167 deletions
@@ -3,11 +3,11 @@
import datetime
import json
import uuid
from unittest.mock import patch
from django.utils import timezone
import pytest
from dirty_equals import IsUUID
from freezegun import freeze_time
from pydantic_ai import ImageUrl
from pydantic_ai.messages import (
@@ -37,27 +37,22 @@ from chat.ai_sdk_types import (
from chat.clients.pydantic_ui_message_converter import model_message_to_ui_message
@pytest.fixture(autouse=True)
def mock_uuid4_fixture():
"""Fixture to mock UUID generation for testing."""
with patch("uuid.uuid4", return_value=uuid.UUID("f0cc3bb5-f207-401b-8281-4cba6202991d")):
yield
def test_model_message_to_ui_message_text_user_full():
"""Test converting a ModelRequest with UserPromptPart containing text to UIMessage."""
timestamp = datetime.datetime.now()
model_message = ModelRequest(
parts=[UserPromptPart(content="Hello!", timestamp=timestamp)], kind="request"
)
result = model_message_to_ui_message(model_message)
expected = UIMessage(
id="f0cc3bb5-f207-401b-8281-4cba6202991d", # Mocked UUID
id=result.id, # Use the generated ID
role="user",
content="Hello!",
parts=[TextUIPart(type="text", text="Hello!")],
createdAt=timestamp,
)
result = model_message_to_ui_message(model_message)
assert result == expected
@@ -65,14 +60,15 @@ def test_model_message_to_ui_message_text_user_full():
def test_model_message_to_ui_message_text_assistant_full():
"""Test converting a ModelResponse with TextPart to UIMessage."""
model_message = ModelResponse(parts=[TextPart(content="Hi there!")])
result = model_message_to_ui_message(model_message)
expected = UIMessage(
id="f0cc3bb5-f207-401b-8281-4cba6202991d", # Mocked UUID
id=result.id, # Use the generated ID
role="assistant",
content="Hi there!",
parts=[TextUIPart(type="text", text="Hi there!")],
createdAt=timezone.now(),
)
result = model_message_to_ui_message(model_message)
assert result == expected
@@ -83,8 +79,10 @@ def test_model_message_to_ui_message_tool_call_full():
model_message = ModelResponse(
parts=[ToolCallPart(tool_call_id="id1", tool_name="tool", args=args)]
)
result = model_message_to_ui_message(model_message)
expected = UIMessage(
id="f0cc3bb5-f207-401b-8281-4cba6202991d", # Mocked UUID
id=result.id, # Use the generated ID
role="assistant",
content="",
parts=[
@@ -100,7 +98,7 @@ def test_model_message_to_ui_message_tool_call_full():
],
createdAt=timezone.now(),
)
result = model_message_to_ui_message(model_message)
assert result == expected
@@ -109,7 +107,7 @@ def test_model_message_to_ui_message_reasoning_full():
"""Test converting a ModelResponse with ThinkingPart to UIMessage."""
model_message = ModelResponse(parts=[ThinkingPart(content="reason", signature="sig")])
expected = UIMessage(
id="f0cc3bb5-f207-401b-8281-4cba6202991d", # Mocked UUID
id=str(uuid.uuid4()), # not used in comparison
role="assistant",
content="",
parts=[
@@ -122,7 +120,7 @@ def test_model_message_to_ui_message_reasoning_full():
createdAt=timezone.now(),
)
result = model_message_to_ui_message(model_message)
assert result.id == expected.id
assert result.id == IsUUID(4)
assert result.role == expected.role
assert result.content == expected.content
assert result.createdAt == expected.createdAt
+11
View File
@@ -0,0 +1,11 @@
"""tools for testing chat functionality"""
import re
def replace_uuids_with_placeholder(text):
"""Replace all UUIDs in the given text with a placeholder."""
text = re.sub('"toolCallId":"([a-z0-9-]){36}"', '"toolCallId":"XXX"', text)
text = re.sub('"toolCallId":"pyd_ai_([a-z0-9]){32}"', '"toolCallId":"pyd_ai_YYY"', text)
text = re.sub('"([a-z0-9-]){36}"', '"<mocked_uuid>"', text)
return text
@@ -1,8 +1,6 @@
"""Common test fixtures for chat conversation endpoint tests."""
import json
import uuid
from unittest.mock import patch
from django.utils import timezone
@@ -12,14 +10,6 @@ import respx
from freezegun import freeze_time
@pytest.fixture(name="mock_uuid4")
def mock_uuid4_fixture():
"""Fixture to mock UUID generation for testing."""
value = uuid.uuid4()
with patch("uuid.uuid4", return_value=value):
yield value
@pytest.fixture(name="mock_openai_stream")
@freeze_time("2025-07-25T10:36:35.297675Z")
def fixture_mock_openai_stream():
@@ -9,6 +9,7 @@ from django.utils import timezone
import pytest
import respx
from asgiref.sync import sync_to_async
from dirty_equals import IsUUID
from freezegun import freeze_time
from rest_framework import status
@@ -23,6 +24,7 @@ from chat.ai_sdk_types import (
)
from chat.factories import ChatConversationFactory
from chat.llm_configuration import LLModel, LLMProvider
from chat.tests.utils import replace_uuids_with_placeholder
# enable database transactions for tests:
# transaction=True ensures that the data are available in the database
@@ -88,7 +90,7 @@ def test_post_conversation_invalid_protocol(api_client):
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_data_protocol(api_client, mock_openai_stream, mock_uuid4):
def test_post_conversation_data_protocol(api_client, mock_openai_stream):
"""Test posting messages to a conversation using the 'data' protocol."""
chat_conversation = ChatConversationFactory(owner__language="en-us")
@@ -115,10 +117,14 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream, mock_uu
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"Hello"\n'
'0:" there"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -137,8 +143,9 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream, mock_uu
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello",
reasoning=None,
@@ -149,8 +156,9 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream, mock_uu
parts=[TextUIPart(type="text", text="Hello")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello there",
reasoning=None,
@@ -216,7 +224,7 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream, mock_uu
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_text_protocol(api_client, mock_openai_stream, mock_uuid4):
def test_post_conversation_text_protocol(api_client, mock_openai_stream):
"""Test posting messages to a conversation using the 'text' protocol."""
chat_conversation = ChatConversationFactory(owner__language="en-us")
@@ -258,8 +266,9 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream, mock_uu
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello",
reasoning=None,
@@ -270,8 +279,9 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream, mock_uu
parts=[TextUIPart(type="text", text="Hello")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello there",
reasoning=None,
@@ -337,7 +347,7 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream, mock_uu
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_with_image(api_client, mock_openai_stream_image, mock_uuid4):
def test_post_conversation_with_image(api_client, mock_openai_stream_image):
"""Ensure an image URL is correctly forwarded to the AI service."""
chat_conversation = ChatConversationFactory(owner__language="en-us")
url = f"/api/v1.0/chats/{chat_conversation.pk}/conversation/?protocol=data"
@@ -375,10 +385,14 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image, mock
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"I see a cat"\n'
'0:" in the picture."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -439,8 +453,9 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image, mock
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello, what do you see on this picture?",
reasoning=None,
@@ -461,8 +476,9 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image, mock
parts=[TextUIPart(type="text", text="Hello, what do you see on this picture?")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="I see a cat in the picture.",
reasoning=None,
@@ -540,7 +556,7 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image, mock
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, mock_uuid4, settings):
def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settings):
"""Ensure tool calls are correctly forwarded and streamed back."""
settings.AI_AGENT_TOOLS = ["get_current_weather"]
@@ -569,6 +585,10 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, mock_u
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'b:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","toolName":'
'"get_current_weather"}\n'
@@ -577,7 +597,7 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, mock_u
'a:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","result":{"location":'
'"Paris","temperature":22,"unit":"celsius"}}\n'
'0:"The current weather in Paris is nice"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -608,8 +628,9 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, mock_u
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Weather in Paris?",
reasoning=None,
@@ -620,8 +641,9 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, mock_u
parts=[TextUIPart(type="text", text="Weather in Paris?")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="The current weather in Paris is nice",
reasoning=None,
@@ -743,9 +765,7 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, mock_u
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_tool_call_fails(
api_client, mock_openai_stream_tool, mock_uuid4, settings
):
def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool, settings):
"""Ensure tool calls are correctly forwarded and streamed back when failing."""
settings.AI_AGENT_TOOLS = []
@@ -774,6 +794,10 @@ def test_post_conversation_tool_call_fails(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'b:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","toolName":"get_current_weather"}\n'
'c:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","argsTextDelta":'
@@ -781,7 +805,7 @@ def test_post_conversation_tool_call_fails(
'a:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","result":"Unknown tool '
"name: 'get_current_weather'. No tools available.\"}\n"
'0:"I cannot give you an answer to that."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -812,8 +836,9 @@ def test_post_conversation_tool_call_fails(
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Weather in Paris?",
reasoning=None,
@@ -824,8 +849,9 @@ def test_post_conversation_tool_call_fails(
parts=[TextUIPart(type="text", text="Weather in Paris?")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="I cannot give you an answer to that.",
reasoning=None,
@@ -972,7 +998,6 @@ def test_post_conversation_model_selection_invalid(api_client):
def test_post_conversation_model_selection_new(
api_client,
mock_openai_stream,
mock_uuid4,
settings,
):
"""Test the user can select a different model."""
@@ -1017,10 +1042,14 @@ def test_post_conversation_model_selection_new(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"Hello"\n'
'0:" there"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -1034,7 +1063,6 @@ def test_post_conversation_model_selection_new(
def test_post_conversation_data_protocol_no_stream(
api_client,
mock_openai_no_stream,
mock_uuid4,
settings,
stream_delay,
):
@@ -1086,6 +1114,9 @@ def test_post_conversation_data_protocol_no_stream(
# Wait for the content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
if stream_delay:
assert response_content == (
'0:"The "\n'
@@ -1105,13 +1136,13 @@ def test_post_conversation_data_protocol_no_stream(
'0:" sca"\n'
'0:"tter"\n'
'0:"ing."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":135}}\n'
)
else:
assert response_content == (
'0:"The sky appears blue due to a phenomenon called Rayleigh scattering."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":135}}\n'
)
@@ -1130,8 +1161,9 @@ def test_post_conversation_data_protocol_no_stream(
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id,
createdAt=timezone.now(), # Mocked timestamp
content="Why the sky is blue?",
reasoning=None,
@@ -1142,8 +1174,9 @@ def test_post_conversation_data_protocol_no_stream(
parts=[TextUIPart(type="text", text="Why the sky is blue?")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="The sky appears blue due to a phenomenon called Rayleigh scattering.",
reasoning=None,
@@ -1222,9 +1255,7 @@ def test_post_conversation_data_protocol_no_stream(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
@pytest.mark.asyncio
async def test_post_conversation_async(
api_client, mock_openai_stream, mock_uuid4, monkeypatch, caplog
):
async def test_post_conversation_async(api_client, mock_openai_stream, monkeypatch, caplog):
"""Test posting messages to a conversation using the 'data' protocol."""
monkeypatch.setenv("PYTHON_SERVER_MODE", "async")
@@ -1261,10 +1292,14 @@ async def test_post_conversation_async(
response_content = b"".join([content async for content in response.streaming_content]).decode(
"utf-8"
)
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"Hello"\n'
'0:" there"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -1283,8 +1318,9 @@ async def test_post_conversation_async(
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[0].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello",
reasoning=None,
@@ -1295,8 +1331,9 @@ async def test_post_conversation_async(
parts=[TextUIPart(type="text", text="Hello")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=chat_conversation.messages[1].id, # don't test the message ID here
createdAt=timezone.now(), # Mocked timestamp
content="Hello there",
reasoning=None,
@@ -14,6 +14,7 @@ import httpx
import pytest
import responses
import respx
from dirty_equals import IsUUID
from freezegun import freeze_time
from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart
from pydantic_ai.models.function import AgentInfo, DeltaToolCall, FunctionModel
@@ -32,6 +33,7 @@ from chat.ai_sdk_types import (
UIMessage,
)
from chat.factories import ChatConversationFactory
from chat.tests.utils import replace_uuids_with_placeholder
# enable database transactions for tests:
# transaction=True ensures that the data are available in the database
@@ -214,12 +216,11 @@ def fixture_mock_openai_stream():
@responses.activate
@respx.mock
@freeze_time()
def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_document_upload( # pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
mock_albert_api, # pylint: disable=unused-argument
sample_pdf_content,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -273,9 +274,11 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
assert response.streaming
# Wait for the streaming content to be fully received
str_mock_uuid4 = str(mock_uuid4)
toolcall_id = f"pyd_ai_{str_mock_uuid4.replace('-', '')}"
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'9:{"toolCallId":"XXX","toolName":"document_parsing",'
'"args":{"documents":[{"identifier":"sample.pdf"}]}}\n'
@@ -283,19 +286,22 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
'b:{"toolCallId":"pyd_ai_YYY","toolName":"document_search_rag"}\n'
'9:{"toolCallId":"pyd_ai_YYY","toolName":"document_search_rag",'
'"args":{"query":"What does the document say?"}}\n'
'h:{"sourceType":"url","id":"XXX","url":"sample.pdf","title":null,"providerMetadata":{}}\n'
'h:{"sourceType":"url","id":"<mocked_uuid>","url":"sample.pdf","title":null,'
'"providerMetadata":{}}\n'
'a:{"toolCallId":"pyd_ai_YYY","result":[{"url":"sample.pdf","content":"This '
'is the content of the PDF.","score":0.9}]}\n'
"0:\"From the document, I can see that it says 'Hello PDF'.\"\n"
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":100,"completionTokens":20}}\n'
).replace("XXX", str_mock_uuid4).replace("pyd_ai_YYY", toolcall_id)
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str_mock_uuid4,
id=chat_conversation.messages[0].id,
createdAt=timezone.now(),
content="What does the document say?",
reasoning=None,
@@ -305,8 +311,10 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
toolInvocations=None,
parts=[TextUIPart(type="text", text="What does the document say?")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str_mock_uuid4,
id=chat_conversation.messages[1].id,
createdAt=timezone.now(),
content="From the document, I can see that it says 'Hello PDF'.",
reasoning=None,
@@ -318,7 +326,7 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
ToolInvocationUIPart(
type="tool-invocation",
toolInvocation=ToolInvocationCall(
toolCallId=toolcall_id,
toolCallId=chat_conversation.messages[1].parts[0].toolInvocation.toolCallId,
toolName="document_search_rag",
args={"query": "What does the document say?"},
state="call",
@@ -330,7 +338,7 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
type="source",
source=LanguageModelV1Source(
sourceType="url",
id=str_mock_uuid4,
id=chat_conversation.messages[1].parts[2].source.id,
url="sample.pdf",
title=None,
providerMetadata={},
@@ -405,7 +413,7 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
"args": '{"query": "What does the document say?"}',
"id": None,
"part_kind": "tool-call",
"tool_call_id": toolcall_id,
"tool_call_id": chat_conversation.pydantic_messages[1]["parts"][0]["tool_call_id"],
"tool_name": "document_search_rag",
}
],
@@ -439,7 +447,7 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
"metadata": {"sources": ["sample.pdf"]},
"part_kind": "tool-return",
"timestamp": timezone_now,
"tool_call_id": toolcall_id,
"tool_call_id": chat_conversation.pydantic_messages[2]["parts"][0]["tool_call_id"],
"tool_name": "document_search_rag",
}
],
@@ -475,13 +483,12 @@ def test_post_conversation_with_document_upload( # noqa: PLR0913 # pylint: disa
@responses.activate
@respx.mock
@freeze_time("2025-07-25T10:36:35.297675Z")
def test_post_conversation_with_document_upload_feature_disabled( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_document_upload_feature_disabled( # pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
caplog,
mock_openai_stream, # pylint: disable=unused-argument
sample_pdf_content,
feature_flags,
mock_uuid4,
):
"""
Test POST to /api/v1/chats/{pk}/conversation/ with a PDF document while feature is disabled.
@@ -526,10 +533,14 @@ def test_post_conversation_with_document_upload_feature_disabled( # noqa: PLR09
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"From the document, I can see that "\n'
"0:\"it says 'Hello PDF'.\"\n"
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":150,"completionTokens":25}}\n'
)
@@ -545,7 +556,6 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
mock_albert_api, # pylint: disable=unused-argument
sample_pdf_content,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
mock_summarization_agent, # pylint: disable=unused-argument
):
@@ -600,29 +610,33 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
assert response.streaming
# Wait for the streaming content to be fully received
str_mock_uuid4 = str(mock_uuid4)
toolcall_id = f"pyd_ai_{str_mock_uuid4.replace('-', '')}"
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'9:{"toolCallId":"XXX","toolName":"document_parsing",'
'"args":{"documents":[{"identifier":"sample.pdf"}]}}\n'
'a:{"toolCallId":"XXX","result":{"state":"done"}}\n'
'b:{"toolCallId":"pyd_ai_YYY","toolName":"summarize"}\n'
'9:{"toolCallId":"pyd_ai_YYY","toolName":"summarize","args":{}}\n'
'h:{"sourceType":"url","id":"XXX","url":"sample.pdf.md",'
'h:{"sourceType":"url","id":"<mocked_uuid>","url":"sample.pdf.md",'
'"title":null,"providerMetadata":{}}\n'
'a:{"toolCallId":"pyd_ai_YYY","result":"The '
'document discusses various topics."}\n'
'0:"The document discusses various topics."\n'
'f:{"messageId":"XXX"}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":201,"completionTokens":13}}\n'
).replace("XXX", str_mock_uuid4).replace("pyd_ai_YYY", toolcall_id)
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str_mock_uuid4,
id=chat_conversation.messages[0].id,
createdAt=timezone.now(),
content="Make a summary of this document.",
reasoning=None,
@@ -632,8 +646,10 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
toolInvocations=None,
parts=[TextUIPart(type="text", text="Make a summary of this document.")],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str_mock_uuid4,
id=chat_conversation.messages[1].id,
createdAt=timezone.now(),
content="The document discusses various topics.",
reasoning=None,
@@ -645,7 +661,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
ToolInvocationUIPart(
type="tool-invocation",
toolInvocation=ToolInvocationCall(
toolCallId=toolcall_id,
toolCallId=chat_conversation.messages[1].parts[0].toolInvocation.toolCallId,
toolName="summarize",
args={},
state="call",
@@ -657,7 +673,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
type="source",
source=LanguageModelV1Source(
sourceType="url",
id=str_mock_uuid4,
id=chat_conversation.messages[1].parts[2].source.id,
url="sample.pdf.md", # might be fixed in the future
title=None,
providerMetadata={},
@@ -732,7 +748,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
"args": "{}",
"id": None,
"part_kind": "tool-call",
"tool_call_id": toolcall_id,
"tool_call_id": chat_conversation.pydantic_messages[1]["parts"][0]["tool_call_id"],
"tool_name": "summarize",
}
],
@@ -760,7 +776,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
"metadata": {"sources": ["sample.pdf.md"]},
"part_kind": "tool-return",
"timestamp": timezone_now,
"tool_call_id": toolcall_id,
"tool_call_id": chat_conversation.pydantic_messages[2]["parts"][0]["tool_call_id"],
"tool_name": "summarize",
}
],
@@ -1,6 +1,8 @@
"""Unit tests for chat conversation actions with document URL."""
# pylint: disable=too-many-lines
import uuid
# pylint: disable=too-many-lines
from io import BytesIO
from django.core.files.storage import default_storage
@@ -8,6 +10,7 @@ from django.utils import formats, timezone
import pytest
import responses
from dirty_equals import IsUUID
from freezegun import freeze_time
from pydantic_ai import ModelRequest, RequestUsage
from pydantic_ai.messages import (
@@ -27,6 +30,7 @@ from chat.ai_sdk_types import (
UIMessage,
)
from chat.factories import ChatConversationAttachmentFactory, ChatConversationFactory
from chat.tests.utils import replace_uuids_with_placeholder
# enable database transactions for tests:
# transaction=True ensures that the data are available in the database
@@ -61,7 +65,6 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
api_client,
sample_document_content,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -162,20 +165,26 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
f'9:{{"toolCallId":"{mock_uuid4}","toolName":"document_parsing",'
'9:{"toolCallId":"XXX","toolName":"document_parsing",'
'"args":{"documents":[{"identifier":"sample.pdf"}]}}\n'
f'a:{{"toolCallId":"{mock_uuid4}","result":{{"state":"done"}}}}\n'
'a:{"toolCallId":"XXX","result":{"state":"done"}}\n'
'0:"This is a document about a single pixel."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":9}}\n'
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[0].id,
createdAt=timezone.now(),
content="What is in this document?",
reasoning=None,
@@ -189,8 +198,10 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
TextUIPart(type="text", text="What is in this document?"),
],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[1].id,
createdAt=timezone.now(),
content="This is a document about a single pixel.",
reasoning=None,
@@ -278,7 +289,6 @@ def test_post_conversation_with_local_pdf_document_url( # pylint: disable=too-m
@freeze_time()
def test_post_conversation_with_local_document_wrong_url(
api_client,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -287,7 +297,7 @@ def test_post_conversation_with_local_document_wrong_url(
chat_conversation = ChatConversationFactory(owner__language="en-us")
api_client.force_authenticate(user=chat_conversation.owner)
document_url = f"/media-key/{mock_uuid4}/sample.pdf"
document_url = f"/media-key/{uuid.uuid4()}/sample.pdf"
message = UIMessage(
id="1",
@@ -326,10 +336,14 @@ def test_post_conversation_with_local_document_wrong_url(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
f'9:{{"toolCallId":"{mock_uuid4}","toolName":"document_parsing",'
'9:{"toolCallId":"XXX","toolName":"document_parsing",'
'"args":{"documents":[{"identifier":"sample.pdf"}]}}\n'
f'a:{{"toolCallId":"{mock_uuid4}",'
'a:{"toolCallId":"XXX",'
'"result":{"state":"error","error":"Document '
'URL does not belong to the conversation."}}\n'
'd:{"finishReason":"error","usage":{"promptTokens":0,"completionTokens":0}}\n'
@@ -343,7 +357,6 @@ def test_post_conversation_with_local_document_wrong_url(
@freeze_time()
def test_post_conversation_with_remote_document_url(
api_client,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -391,10 +404,14 @@ def test_post_conversation_with_remote_document_url(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
f'9:{{"toolCallId":"{mock_uuid4}","toolName":"document_parsing",'
'9:{"toolCallId":"XXX","toolName":"document_parsing",'
'"args":{"documents":[{"identifier":"sample.pdf"}]}}\n'
f'a:{{"toolCallId":"{mock_uuid4}",'
'a:{"toolCallId":"XXX",'
'"result":{"state":"error","error":"External document '
'URL are not accepted yet."}}\n'
'd:{"finishReason":"error","usage":{"promptTokens":0,"completionTokens":0}}\n'
@@ -409,7 +426,6 @@ def test_post_conversation_with_remote_document_url(
def test_post_conversation_with_local_document_url_in_history( # pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -422,7 +438,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
owner__language="en-us",
messages=[
UIMessage(
id=str(mock_uuid4),
id=str(uuid.uuid4()),
createdAt=timezone.now(),
content="What is in this document?",
reasoning=None,
@@ -437,7 +453,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
],
),
UIMessage(
id=str(mock_uuid4),
id=str(uuid.uuid4()),
createdAt=timezone.now(),
content="This is a document about a single pixel.",
reasoning=None,
@@ -603,17 +619,23 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"This is a document of square, very small and nice."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":11}}\n'
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2 + 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[0].id,
createdAt=timezone.now(),
content="What is in this document?",
reasoning=None,
@@ -627,8 +649,10 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
TextUIPart(type="text", text="What is in this document?"),
],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[1].id,
createdAt=timezone.now(),
content="This is a document about a single pixel.",
reasoning=None,
@@ -640,8 +664,10 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
TextUIPart(type="text", text="This is a document about a single pixel."),
],
)
assert chat_conversation.messages[2].id == IsUUID(4)
assert chat_conversation.messages[2] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[2].id,
createdAt=timezone.now(),
content="Give more details about this document.",
reasoning=None,
@@ -653,8 +679,10 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
TextUIPart(type="text", text="Give more details about this document."),
],
)
assert chat_conversation.messages[3].id == IsUUID(4)
assert chat_conversation.messages[3] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[3].id,
createdAt=timezone.now(),
content="This is a document of square, very small and nice.",
reasoning=None,
@@ -783,10 +811,9 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
("data.csv", "text/csv"),
],
)
def test_post_conversation_with_local_not_pdf_document_url( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments
def test_post_conversation_with_local_not_pdf_document_url( # pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
file_name,
content_type,
@@ -901,20 +928,26 @@ def test_post_conversation_with_local_not_pdf_document_url( # noqa: PLR0913 # p
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
f'9:{{"toolCallId":"{mock_uuid4}","toolName":"document_parsing",'
'9:{"toolCallId":"XXX","toolName":"document_parsing",'
f'"args":{{"documents":[{{"identifier":"{file_name}"}}]}}}}\n'
f'a:{{"toolCallId":"{mock_uuid4}","result":{{"state":"done"}}}}\n'
'a:{"toolCallId":"XXX","result":{"state":"done"}}\n'
'0:"This is a document about you."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":7}}\n'
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[0].id,
createdAt=timezone.now(),
content="What is in this document?",
reasoning=None,
@@ -926,8 +959,10 @@ def test_post_conversation_with_local_not_pdf_document_url( # noqa: PLR0913 # p
TextUIPart(type="text", text="What is in this document?"),
],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[1].id,
createdAt=timezone.now(),
content="This is a document about you.",
reasoning=None,
@@ -7,6 +7,7 @@ from django.utils import timezone
import pytest
import respx
from dirty_equals import IsUUID
from freezegun import freeze_time
from rest_framework import status
@@ -18,6 +19,7 @@ from chat.ai_sdk_types import (
UIMessage,
)
from chat.factories import ChatConversationFactory
from chat.tests.utils import replace_uuids_with_placeholder
# enable database transactions for tests:
# transaction=True ensures that the data are available in the database
@@ -200,7 +202,7 @@ def history_conversation_fixture():
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_data_protocol_with_history(
api_client, mock_openai_stream, mock_uuid4, history_conversation
api_client, mock_openai_stream, history_conversation
):
"""Test posting messages to a conversation with history using the 'data' protocol."""
url = f"/api/v1.0/chats/{history_conversation.pk}/conversation/?protocol=data"
@@ -226,10 +228,14 @@ def test_post_conversation_data_protocol_with_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"Hello"\n'
'0:" there"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -259,8 +265,9 @@ def test_post_conversation_data_protocol_with_history(
assert len(history_conversation.messages) == 6
# Verify the most recent message is the new one
assert history_conversation.messages[4].id == IsUUID(4)
assert history_conversation.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="Hello",
reasoning=None,
@@ -271,8 +278,9 @@ def test_post_conversation_data_protocol_with_history(
parts=[TextUIPart(type="text", text="Hello")],
)
assert history_conversation.messages[5].id == IsUUID(4)
assert history_conversation.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="Hello there",
reasoning=None,
@@ -290,7 +298,7 @@ def test_post_conversation_data_protocol_with_history(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_text_protocol_with_history(
api_client, mock_openai_stream, mock_uuid4, history_conversation
api_client, mock_openai_stream, history_conversation
):
"""Test posting messages to a conversation with history using the 'text' protocol."""
url = f"/api/v1.0/chats/{history_conversation.pk}/conversation/?protocol=text"
@@ -335,8 +343,9 @@ def test_post_conversation_text_protocol_with_history(
assert len(history_conversation.messages) == 6
# Verify the most recent messages are the new ones
assert history_conversation.messages[4].id == IsUUID(4)
assert history_conversation.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="Hello",
reasoning=None,
@@ -347,8 +356,9 @@ def test_post_conversation_text_protocol_with_history(
parts=[TextUIPart(type="text", text="Hello")],
)
assert history_conversation.messages[5].id == IsUUID(4)
assert history_conversation.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="Hello there",
reasoning=None,
@@ -363,7 +373,7 @@ def test_post_conversation_text_protocol_with_history(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_with_image_with_history(
api_client, mock_openai_stream_image, mock_uuid4, history_conversation
api_client, mock_openai_stream_image, history_conversation
):
"""
Ensure an image URL is correctly forwarded to the AI service with a conversation with history.
@@ -403,10 +413,14 @@ def test_post_conversation_with_image_with_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"I see a cat"\n'
'0:" in the picture."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -452,8 +466,9 @@ def test_post_conversation_with_image_with_history(
assert len(history_conversation.messages) == 6
# Verify the most recent message has the image attachment
assert history_conversation.messages[4].id == IsUUID(4)
assert history_conversation.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="Hello, what do you see on this picture?",
reasoning=None,
@@ -474,8 +489,9 @@ def test_post_conversation_with_image_with_history(
parts=[TextUIPart(type="text", text="Hello, what do you see on this picture?")],
)
assert history_conversation.messages[5].id == IsUUID(4)
assert history_conversation.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="I see a cat in the picture.",
reasoning=None,
@@ -490,7 +506,7 @@ def test_post_conversation_with_image_with_history(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_tool_call_with_history(
api_client, mock_openai_stream_tool, mock_uuid4, settings, history_conversation
api_client, mock_openai_stream_tool, settings, history_conversation
):
"""
Ensure tool calls are correctly forwarded and streamed back with a conversation with history.
@@ -521,6 +537,10 @@ def test_post_conversation_tool_call_with_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'b:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","toolName":'
'"get_current_weather"}\n'
@@ -529,7 +549,7 @@ def test_post_conversation_tool_call_with_history(
'a:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","result":{"location":'
'"Paris","temperature":22,"unit":"celsius"}}\n'
'0:"The current weather in Paris is nice"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -561,8 +581,9 @@ def test_post_conversation_tool_call_with_history(
assert len(history_conversation.messages) == 6
# Verify the most recent message is the new one with tool invocation
assert history_conversation.messages[4].id == IsUUID(4)
assert history_conversation.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="Weather in Paris?",
reasoning=None,
@@ -573,8 +594,9 @@ def test_post_conversation_tool_call_with_history(
parts=[TextUIPart(type="text", text="Weather in Paris?")],
)
assert history_conversation.messages[5].id == IsUUID(4)
assert history_conversation.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="The current weather in Paris is nice",
reasoning=None,
@@ -606,7 +628,7 @@ def test_post_conversation_tool_call_with_history(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_tool_call_fails_with_history(
api_client, mock_openai_stream_tool, mock_uuid4, settings, history_conversation
api_client, mock_openai_stream_tool, settings, history_conversation
):
"""
Ensure tool calls are correctly forwarded and streamed back when failing with a
@@ -638,6 +660,10 @@ def test_post_conversation_tool_call_fails_with_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'b:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","toolName":'
'"get_current_weather"}\n'
@@ -646,7 +672,7 @@ def test_post_conversation_tool_call_fails_with_history(
'a:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","result":"Unknown tool '
"name: 'get_current_weather'. No tools available.\"}\n"
'0:"I cannot give you an answer to that."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -678,8 +704,9 @@ def test_post_conversation_tool_call_fails_with_history(
assert len(history_conversation.messages) == 6
# Verify the most recent message is the new one with tool invocation
assert history_conversation.messages[4].id == IsUUID(4)
assert history_conversation.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="Weather in Paris?",
reasoning=None,
@@ -690,8 +717,9 @@ def test_post_conversation_tool_call_fails_with_history(
parts=[TextUIPart(type="text", text="Weather in Paris?")],
)
assert history_conversation.messages[5].id == IsUUID(4)
assert history_conversation.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="I cannot give you an answer to that.",
reasoning=None,
@@ -1147,7 +1175,7 @@ def history_conversation_with_tool_fixture():
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_with_existing_image_history(
api_client, mock_openai_stream, mock_uuid4, history_conversation_with_image
api_client, mock_openai_stream, history_conversation_with_image
):
"""Test posting a message to a conversation that already has images in its history."""
url = f"/api/v1.0/chats/{history_conversation_with_image.pk}/conversation/?protocol=data"
@@ -1173,10 +1201,14 @@ def test_post_conversation_with_existing_image_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"Hello"\n'
'0:" there"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -1207,8 +1239,9 @@ def test_post_conversation_with_existing_image_history(
assert len(history_conversation_with_image.messages) == 6
# Verify the most recent messages are the new ones
assert history_conversation_with_image.messages[4].id == IsUUID(4)
assert history_conversation_with_image.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation_with_image.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="What was in that image again?",
reasoning=None,
@@ -1219,8 +1252,9 @@ def test_post_conversation_with_existing_image_history(
parts=[TextUIPart(type="text", text="What was in that image again?")],
)
assert history_conversation_with_image.messages[5].id == IsUUID(4)
assert history_conversation_with_image.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation_with_image.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="Hello there",
reasoning=None,
@@ -1238,7 +1272,7 @@ def test_post_conversation_with_existing_image_history(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_with_existing_tool_history(
api_client, mock_openai_stream_tool, mock_uuid4, settings, history_conversation_with_tool
api_client, mock_openai_stream_tool, settings, history_conversation_with_tool
):
"""Test posting a message to a conversation that already has tool calls in its history."""
settings.AI_AGENT_TOOLS = ["get_current_weather"]
@@ -1266,6 +1300,10 @@ def test_post_conversation_with_existing_tool_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'b:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","toolName":'
'"get_current_weather"}\n'
@@ -1274,7 +1312,7 @@ def test_post_conversation_with_existing_tool_history(
'a:{"toolCallId":"xLDcIljdsDrz0idal7tATWSMm2jhMj47","result":{"location":'
'"Paris","temperature":22,"unit":"celsius"}}\n'
'0:"The current weather in Paris is nice"\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -1294,8 +1332,9 @@ def test_post_conversation_with_existing_tool_history(
assert len(history_conversation_with_tool.messages) == 6
# Verify the most recent message is the new one with tool invocation
assert history_conversation_with_tool.messages[4].id == IsUUID(4)
assert history_conversation_with_tool.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation_with_tool.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="How about Paris weather?",
reasoning=None,
@@ -1306,8 +1345,9 @@ def test_post_conversation_with_existing_tool_history(
parts=[TextUIPart(type="text", text="How about Paris weather?")],
)
assert history_conversation_with_tool.messages[5].id == IsUUID(4)
assert history_conversation_with_tool.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation_with_tool.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="The current weather in Paris is nice",
reasoning=None,
@@ -1417,7 +1457,7 @@ def test_post_conversation_with_existing_tool_history(
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_add_image_to_conversation_with_tool_history(
api_client, mock_openai_stream_image, mock_uuid4, history_conversation_with_tool
api_client, mock_openai_stream_image, history_conversation_with_tool
):
"""Test adding an image to a conversation that already has tool calls in its history."""
url = f"/api/v1.0/chats/{history_conversation_with_tool.pk}/conversation/?protocol=data"
@@ -1455,10 +1495,14 @@ def test_post_conversation_add_image_to_conversation_with_tool_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"I see a cat"\n'
'0:" in the picture."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
@@ -1484,8 +1528,9 @@ def test_post_conversation_add_image_to_conversation_with_tool_history(
assert len(history_conversation_with_tool.messages) == 6
# Verify the most recent message has the image attachment
assert history_conversation_with_tool.messages[4].id == IsUUID(4)
assert history_conversation_with_tool.messages[4] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation_with_tool.messages[4].id,
createdAt=timezone.now(), # Mocked timestamp
content="How's the weather in this image?",
reasoning=None,
@@ -1506,8 +1551,9 @@ def test_post_conversation_add_image_to_conversation_with_tool_history(
parts=[TextUIPart(type="text", text="How's the weather in this image?")],
)
assert history_conversation_with_tool.messages[5].id == IsUUID(4)
assert history_conversation_with_tool.messages[5] == UIMessage(
id=str(mock_uuid4), # Mocked UUID
id=history_conversation_with_tool.messages[5].id,
createdAt=timezone.now(), # Mocked timestamp
content="I see a cat in the picture.",
reasoning=None,
@@ -1,8 +1,11 @@
"""Unit tests for chat conversation actions with image URL."""
import uuid
from django.utils import timezone
import pytest
from dirty_equals import IsUUID
from freezegun import freeze_time
from pydantic_ai import ModelRequest, RequestUsage
from pydantic_ai.messages import (
@@ -22,6 +25,7 @@ from chat.ai_sdk_types import (
UIMessage,
)
from chat.factories import ChatConversationFactory
from chat.tests.utils import replace_uuids_with_placeholder
# enable database transactions for tests:
# transaction=True ensures that the data are available in the database
@@ -53,7 +57,6 @@ def fixture_sample_image_content():
@freeze_time("2025-10-18T20:48:20.286204Z")
def test_post_conversation_with_local_image_url(
api_client,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -131,17 +134,23 @@ def test_post_conversation_with_local_image_url(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"This is an image of a single pixel."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":9}}\n'
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[0].id, # don't test the value directly
createdAt=timezone.now(),
content="What is in this image?",
reasoning=None,
@@ -155,8 +164,10 @@ def test_post_conversation_with_local_image_url(
TextUIPart(type="text", text="What is in this image?"),
],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[1].id, # don't test the value directly
createdAt=timezone.now(),
content="This is an image of a single pixel.",
reasoning=None,
@@ -238,7 +249,6 @@ def test_post_conversation_with_local_image_url(
def test_post_conversation_with_local_image_wrong_url(
api_client,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -247,7 +257,7 @@ def test_post_conversation_with_local_image_wrong_url(
chat_conversation = ChatConversationFactory(owner__language="en-us")
api_client.force_authenticate(user=chat_conversation.owner)
image_url = f"/media-key/{mock_uuid4}/sample.png"
image_url = f"/media-key/{uuid.uuid4()}/sample.png"
message = UIMessage(
id="1",
@@ -308,9 +318,13 @@ def test_post_conversation_with_local_image_wrong_url(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"cannot read image."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":4}}\n'
)
@@ -322,7 +336,6 @@ def test_post_conversation_with_local_image_wrong_url(
def test_post_conversation_with_remote_image_url(
api_client,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -392,17 +405,23 @@ def test_post_conversation_with_remote_image_url(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"This is an image of a single pixel."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":9}}\n'
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[0].id, # don't test the value directly
createdAt=timezone.now(),
content="What is in this image?",
reasoning=None,
@@ -416,8 +435,10 @@ def test_post_conversation_with_remote_image_url(
TextUIPart(type="text", text="What is in this image?"),
],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[1].id, # don't test the value directly
createdAt=timezone.now(),
content="This is an image of a single pixel.",
reasoning=None,
@@ -435,7 +456,6 @@ def test_post_conversation_with_remote_image_url(
def test_post_conversation_with_local_image_url_in_history(
api_client,
today_promt_date,
mock_uuid4,
mock_ai_agent_service,
):
"""
@@ -448,7 +468,7 @@ def test_post_conversation_with_local_image_url_in_history(
owner__language="en-us",
messages=[
UIMessage(
id=str(mock_uuid4),
id=str(uuid.uuid4()),
createdAt=timezone.now(),
content="What is in this image?",
reasoning=None,
@@ -463,7 +483,7 @@ def test_post_conversation_with_local_image_url_in_history(
],
),
UIMessage(
id=str(mock_uuid4),
id=str(uuid.uuid4()),
createdAt=timezone.now(),
content="This is an image of a single pixel.",
reasoning=None,
@@ -629,17 +649,23 @@ def test_post_conversation_with_local_image_url_in_history(
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"This is an image of square, very small and nice."\n'
f'f:{{"messageId":"{mock_uuid4}"}}\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":50,"completionTokens":11}}\n'
)
# Check that the conversation was updated
chat_conversation.refresh_from_db()
assert len(chat_conversation.messages) == 2 + 2
assert chat_conversation.messages[0].id == IsUUID(4)
assert chat_conversation.messages[0] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[0].id, # don't test the value directly
createdAt=timezone.now(),
content="What is in this image?",
reasoning=None,
@@ -653,8 +679,10 @@ def test_post_conversation_with_local_image_url_in_history(
TextUIPart(type="text", text="What is in this image?"),
],
)
assert chat_conversation.messages[1].id == IsUUID(4)
assert chat_conversation.messages[1] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[1].id, # don't test the value directly
createdAt=timezone.now(),
content="This is an image of a single pixel.",
reasoning=None,
@@ -666,8 +694,10 @@ def test_post_conversation_with_local_image_url_in_history(
TextUIPart(type="text", text="This is an image of a single pixel."),
],
)
assert chat_conversation.messages[2].id == IsUUID(4)
assert chat_conversation.messages[2] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[2].id, # don't test the value directly
createdAt=timezone.now(),
content="Give more details about this image.",
reasoning=None,
@@ -679,8 +709,10 @@ def test_post_conversation_with_local_image_url_in_history(
TextUIPart(type="text", text="Give more details about this image."),
],
)
assert chat_conversation.messages[3].id == IsUUID(4)
assert chat_conversation.messages[3] == UIMessage(
id=str(mock_uuid4),
id=chat_conversation.messages[3].id, # don't test the value directly
createdAt=timezone.now(),
content="This is an image of square, very small and nice.",
reasoning=None,
+1
View File
@@ -75,6 +75,7 @@ dependencies = [
[project.optional-dependencies]
dev = [
"dirty-equals==0.10.0",
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2025.10.1",