From 34cf348f4c6b65c993aef82591cc66de9747a96a Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Mon, 27 Oct 2025 21:37:36 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(pydantic-ai)=20update=20tests=20after?= =?UTF-8?q?=20last=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../test_model_message_to_ui_message.py | 30 +++-- src/backend/chat/tests/utils.py | 11 ++ .../views/chat/conversations/conftest.py | 10 -- .../chat/conversations/test_conversation.py | 105 +++++++++++------ .../test_conversation_with_document_upload.py | 72 +++++++----- .../test_conversation_with_document_url.py | 93 ++++++++++----- .../test_conversation_with_history.py | 108 +++++++++++++----- .../test_conversation_with_image_url.py | 70 +++++++++--- src/backend/pyproject.toml | 1 + 9 files changed, 333 insertions(+), 167 deletions(-) create mode 100644 src/backend/chat/tests/utils.py diff --git a/src/backend/chat/tests/clients/pydantic_ui_message_converter/test_model_message_to_ui_message.py b/src/backend/chat/tests/clients/pydantic_ui_message_converter/test_model_message_to_ui_message.py index 5b7ee0a..cc01824 100644 --- a/src/backend/chat/tests/clients/pydantic_ui_message_converter/test_model_message_to_ui_message.py +++ b/src/backend/chat/tests/clients/pydantic_ui_message_converter/test_model_message_to_ui_message.py @@ -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 diff --git a/src/backend/chat/tests/utils.py b/src/backend/chat/tests/utils.py new file mode 100644 index 0000000..d41d4d7 --- /dev/null +++ b/src/backend/chat/tests/utils.py @@ -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}"', '""', text) + return text diff --git a/src/backend/chat/tests/views/chat/conversations/conftest.py b/src/backend/chat/tests/views/chat/conversations/conftest.py index beeb79f..1f81305 100644 --- a/src/backend/chat/tests/views/chat/conversations/conftest.py +++ b/src/backend/chat/tests/views/chat/conversations/conftest.py @@ -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(): diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation.py b/src/backend/chat/tests/views/chat/conversations/test_conversation.py index 44c3336..f4098c8 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation.py @@ -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":""}\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":""}\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":""}\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":""}\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":""}\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":""}\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":""}\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":""}\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, diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py index 9ee0346..6958940 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py @@ -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":"","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":""}\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":""}\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":"","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":""}\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", } ], diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py index 0dd6511..fd1877e 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_url.py @@ -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":""}\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":""}\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":""}\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, diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py index 23ea90a..ce2475b 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_history.py @@ -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":""}\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":""}\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":""}\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":""}\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":""}\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":""}\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":""}\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, diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py index eedcd4d..26eee2a 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_image_url.py @@ -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":""}\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":""}\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":""}\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":""}\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, diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index d5932a3..677a948 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -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",