@@ -988,7 +988,11 @@ def handler(req):
988988
989989class TestChatE2E :
990990 @staticmethod
991- def _sse_body (chunks : list [str ], conv_id : str = "conv-42" , msg_id : str = "msg-1" ) -> str :
991+ def _sse_body (
992+ chunks : list [str ],
993+ conv_id : str = "69fceb3e7b2a6a7efdd18180" ,
994+ msg_id : str = "69fceb3e7b2a6a7efdd18181" ,
995+ ) -> str :
992996 """Build an SSE response body with metadata + content chunks + DONE."""
993997 lines = [
994998 "event: message" ,
@@ -1011,6 +1015,7 @@ def handler(req):
10111015 data = json .loads (req .content )
10121016 assert data ["stream" ] is True
10131017 assert data ["messages" ][0 ]["content" ] == "How does auth work?"
1018+ assert req .headers ["accept" ] == "text/event-stream, application/problem+json"
10141019 return httpx .Response (200 , text = body , headers = {"content-type" : "text/event-stream" })
10151020
10161021 mcp = _server ({"/api/chat/completions" : handler })
@@ -1023,27 +1028,42 @@ def handler(req):
10231028 text = _text (result )
10241029 assert "Hello world!" in text
10251030 # New conversation gets ID appended
1026- assert "conv-42 " in text
1031+ assert "69fceb3e7b2a6a7efdd18180 " in text
10271032
10281033 @pytest .mark .asyncio
10291034 async def test_continuing_conversation (self ):
1030- body = self ._sse_body (["Follow-up answer" ], conv_id = "conv-existing" )
1035+ conversation_id = "69fceb3e7b2a6a7efdd18180"
1036+ body = self ._sse_body (["Follow-up answer" ], conv_id = conversation_id )
10311037
10321038 def handler (req ):
10331039 data = json .loads (req .content )
1034- assert data ["conversationId" ] == "conv-existing"
1040+ assert data ["conversationId" ] == conversation_id
10351041 return httpx .Response (200 , text = body , headers = {"content-type" : "text/event-stream" })
10361042
10371043 mcp = _server ({"/api/chat/completions" : handler })
10381044 async with Client (mcp ) as client :
10391045 result = await client .call_tool (
10401046 "chat" ,
1041- {"question" : "And the error handling?" , "conversation_id" : "conv-existing" },
1047+ {"question" : "And the error handling?" , "conversation_id" : conversation_id },
10421048 )
10431049
10441050 text = _text (result )
10451051 assert "Follow-up answer" in text
10461052
1053+ @pytest .mark .asyncio
1054+ async def test_invalid_conversation_id_returns_actionable_tool_error (self ):
1055+ mcp = _server ({})
1056+ async with Client (mcp ) as client :
1057+ result = await client .call_tool (
1058+ "chat" ,
1059+ {"question" : "And the error handling?" , "conversation_id" : "conv-existing" },
1060+ raise_on_error = False ,
1061+ )
1062+
1063+ text = _text (result )
1064+ assert "24-character hex Mongo ObjectId" in text
1065+ assert "Retry: no" in text
1066+
10471067 @pytest .mark .asyncio
10481068 async def test_empty_question_returns_error (self ):
10491069 mcp = _server ({})
@@ -1071,6 +1091,97 @@ async def test_backend_error_handled(self):
10711091 text = _text (result )
10721092 assert "401" in text or "auth" in text .lower ()
10731093
1094+ @pytest .mark .asyncio
1095+ async def test_problem_details_backend_error_keeps_detail_and_request_id (self ):
1096+ problem = {
1097+ "type" : "https://app.codealive.ai/errors/bad-request" ,
1098+ "title" : "Bad request" ,
1099+ "status" : 400 ,
1100+ "detail" : "Message content violates our content policy" ,
1101+ "requestId" : "req-rest" ,
1102+ }
1103+
1104+ mcp = _server ({
1105+ "/api/chat/completions" : lambda r : httpx .Response (
1106+ 400 ,
1107+ json = problem ,
1108+ headers = {"content-type" : "application/problem+json" },
1109+ ),
1110+ })
1111+ async with Client (mcp ) as client :
1112+ result = await client .call_tool (
1113+ "chat" ,
1114+ {"question" : "hello" },
1115+ raise_on_error = False ,
1116+ )
1117+
1118+ text = _text (result )
1119+ assert "Message content violates our content policy" in text
1120+ assert "requestId=req-rest" in text
1121+ assert "Retry: no" in text
1122+
1123+ @pytest .mark .asyncio
1124+ async def test_named_sse_problem_details_error_returns_tool_error (self ):
1125+ problem = json .dumps ({
1126+ "type" : "https://app.codealive.ai/errors/bad-request" ,
1127+ "title" : "Bad request" ,
1128+ "status" : 400 ,
1129+ "detail" : "Message content violates our content policy" ,
1130+ "requestId" : "req-sse" ,
1131+ })
1132+ body = f"event: error\n data: { problem } \n \n "
1133+
1134+ mcp = _server ({
1135+ "/api/chat/completions" : lambda r : httpx .Response (
1136+ 200 ,
1137+ text = body ,
1138+ headers = {"content-type" : "text/event-stream" },
1139+ ),
1140+ })
1141+ async with Client (mcp ) as client :
1142+ result = await client .call_tool (
1143+ "chat" ,
1144+ {"question" : "hello" , "data_sources" : ["backend" ]},
1145+ raise_on_error = False ,
1146+ )
1147+
1148+ text = _text (result )
1149+ assert "Message content violates our content policy" in text
1150+ assert "Code: 400" in text
1151+ assert "requestId=req-sse" in text
1152+ assert "Retry: no" in text
1153+
1154+ @pytest .mark .asyncio
1155+ async def test_named_sse_rate_limit_error_is_retryable (self ):
1156+ problem = json .dumps ({
1157+ "type" : "https://app.codealive.ai/errors/plan-limit" ,
1158+ "title" : "Plan limit" ,
1159+ "status" : 429 ,
1160+ "detail" : "Chat completion rate limit exceeded" ,
1161+ "requestId" : "req-sse-429" ,
1162+ })
1163+ body = f"event: error\n data: { problem } \n \n "
1164+
1165+ mcp = _server ({
1166+ "/api/chat/completions" : lambda r : httpx .Response (
1167+ 200 ,
1168+ text = body ,
1169+ headers = {"content-type" : "text/event-stream" },
1170+ ),
1171+ })
1172+ async with Client (mcp ) as client :
1173+ result = await client .call_tool (
1174+ "chat" ,
1175+ {"question" : "hello" , "data_sources" : ["backend" ]},
1176+ raise_on_error = False ,
1177+ )
1178+
1179+ text = _text (result )
1180+ assert "Chat completion rate limit exceeded" in text
1181+ assert "Retry: yes" in text
1182+ assert "back off" in text
1183+ assert "requestId=req-sse-429" in text
1184+
10741185 @pytest .mark .asyncio
10751186 async def test_unicode_preserved_in_streamed_response (self ):
10761187 """Cyrillic chunks streamed via SSE must survive as UTF-8 in the final text."""
0 commit comments