1010import  json 
1111import  os 
1212import  sys 
13+ import  unittest 
1314import  uuid 
1415
1516import  requests 
1617
17- # Add parent directory to path to allow importing common test utilities 
18- sys .path .append (os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))))
19- from  tests .test_base  import  SemanticRouterTestBase 
18+ # Import test base from same directory 
19+ from  test_base  import  SemanticRouterTestBase 
2020
2121# Constants 
2222ENVOY_URL  =  "http://localhost:8801" 
2323OPENAI_ENDPOINT  =  "/v1/chat/completions" 
24- DEFAULT_MODEL  =  "qwen2.5:32b "   # Changed from gemma3:27b to match make test-prompt  
24+ DEFAULT_MODEL  =  "Model-A "   # Use configured model that matches router config  
2525
2626
2727class  EnvoyExtProcTest (SemanticRouterTestBase ):
@@ -35,11 +35,13 @@ def setUp(self):
3535        )
3636
3737        try :
38+             # Use unique content to bypass cache for setup check 
39+             setup_id  =  str (uuid .uuid4 ())[:8 ]
3840            payload  =  {
3941                "model" : DEFAULT_MODEL ,
4042                "messages" : [
41-                     {"role" : "assistant " , "content" : "You are a helpful assistant." },
42-                     {"role" : "user" , "content" : " test" },
43+                     {"role" : "system " , "content" : "You are a helpful assistant." },
44+                     {"role" : "user" , "content" : f"ExtProc setup  test  { setup_id }  " },
4345                ],
4446            }
4547
@@ -77,8 +79,11 @@ def test_request_headers_propagation(self):
7779        payload  =  {
7880            "model" : DEFAULT_MODEL ,
7981            "messages" : [
80-                 {"role" : "assistant" , "content" : "You are a helpful assistant." },
81-                 {"role" : "user" , "content" : "What is the capital of France?" },
82+                 {"role" : "system" , "content" : "You are a helpful assistant." },
83+                 {
84+                     "role" : "user" ,
85+                     "content" : f"ExtProc header test { trace_id [:8 ]}   - explain photosynthesis briefly." ,
86+                 },
8287            ],
8388            "temperature" : 0.7 ,
8489        }
@@ -137,158 +142,225 @@ def test_request_headers_propagation(self):
137142        )
138143        self .assertIn ("model" , response_json , "Response is missing 'model' field" )
139144
140-     def  test_extproc_override (self ):
141-         """Test that the ExtProc can modify the request's target model .""" 
145+     def  test_extproc_body_modification (self ):
146+         """Test that the ExtProc can modify the request and response bodies .""" 
142147        self .print_test_header (
143-             "ExtProc Model Override  Test" ,
144-             "Verifies that ExtProc correctly routes different query types to appropriate models " ,
148+             "ExtProc Body Modification  Test" ,
149+             "Verifies that ExtProc can modify request and response bodies while preserving essential fields " ,
145150        )
146151
147-         test_cases  =  [
148-             {
149-                 "name" : "Math Query" ,
150-                 "content" : "What is the derivative of f(x) = x^3 + 2x^2 - 5x + 7?" ,
151-                 "category" : "math" ,
152-             },
152+         trace_id  =  str (uuid .uuid4 ())
153+ 
154+         payload  =  {
155+             "model" : DEFAULT_MODEL ,
156+             "messages" : [
157+                 {"role" : "system" , "content" : "You are a helpful assistant." },
158+                 {
159+                     "role" : "user" ,
160+                     "content" : f"ExtProc body test { trace_id [:8 ]}   - describe machine learning in simple terms." ,
161+                 },
162+             ],
163+             "temperature" : 0.7 ,
164+             "test_field" : "should_be_preserved" ,
165+         }
166+ 
167+         headers  =  {
168+             "Content-Type" : "application/json" ,
169+             "X-Test-Trace-ID" : trace_id ,
170+             "X-Test-Body-Modification" : "true" ,
171+         }
172+ 
173+         self .print_request_info (
174+             payload = payload ,
175+             expectations = "Expect: Request processing with body modifications while preserving essential fields" ,
176+         )
177+ 
178+         response  =  requests .post (
179+             f"{ ENVOY_URL } { OPENAI_ENDPOINT }  " , headers = headers , json = payload , timeout = 60 
180+         )
181+ 
182+         response_json  =  response .json ()
183+         self .print_response_info (
184+             response ,
153185            {
154-                 "name " : "Creative Writing Query" ,
155-                 "content " : "Write a short story about a space cat." ,
156-                 "category " : "creative"  ,
186+                 "Original Model " : DEFAULT_MODEL ,
187+                 "Final Model " : response_json . get ( "model" ,  "Not specified" ) ,
188+                 "Test Field Preserved " : "test_field"    in   response_json ,
157189            },
158-         ] 
190+         ) 
159191
160-         results  =  {}
192+         passed  =  response .status_code  <  400  and  "model"  in  response_json 
193+         self .print_test_result (
194+             passed = passed ,
195+             message = (
196+                 "Request processed successfully with body modifications" 
197+                 if  passed 
198+                 else  "Issues with request processing or body modifications" 
199+             ),
200+         )
161201
162-         for  test_case  in  test_cases :
163-             self .print_subtest_header (test_case ["name" ])
202+         self .assertLess (
203+             response .status_code ,
204+             400 ,
205+             f"Request was rejected with status code { response .status_code }  " ,
206+         )
164207
165-             trace_id  =  str (uuid .uuid4 ())
208+     def  test_extproc_error_handling (self ):
209+         """Test ExtProc error handling and failure scenarios.""" 
210+         self .print_test_header (
211+             "ExtProc Error Handling Test" ,
212+             "Verifies that ExtProc properly handles and recovers from error conditions" ,
213+         )
166214
167-             payload  =  {
168-                 "model" : DEFAULT_MODEL ,
169-                 "messages" : [
170-                     {
171-                         "role" : "assistant" ,
172-                         "content" : f"You are an expert in { test_case ['category' ]}  ." ,
173-                     },
174-                     {"role" : "user" , "content" : test_case ["content" ]},
175-                 ],
176-                 "temperature" : 0.7 ,
177-             }
215+         # Test with headers that might cause ExtProc issues 
216+         payload  =  {
217+             "model" : DEFAULT_MODEL ,
218+             "messages" : [
219+                 {"role" : "system" , "content" : "You are a helpful assistant." },
220+                 {"role" : "user" , "content" : "Simple test query" },
221+             ],
222+         }
178223
179-              headers  =  {
180-                  "Content-Type" : "application/json" ,
181-                  "X-Test-Trace-ID " : trace_id , 
182-                  "X-Original-Model " : DEFAULT_MODEL ,
183-                  "X-Test-Category " : test_case [ "category" ], 
184-              }
224+         headers  =  {
225+             "Content-Type" : "application/json" ,
226+             "X-Very-Long-Header " : "x"   *   1000 ,   # Very long header value 
227+             "X-Test-Error-Recovery " : "true" ,
228+             "X-Special-Chars " : "data-with-special-chars-!@#$%^&*()" ,   # Special characters 
229+         }
185230
186-              self .print_request_info (
187-                  payload = payload ,
188-                  expectations = f "Expect: Query  to be routed based on  { test_case [ 'category' ] }  category " ,
189-              )
231+         self .print_request_info (
232+             payload = payload ,
233+             expectations = "Expect: ExtProc  to handle unusual headers gracefully without crashing " ,
234+         )
190235
236+         try :
191237            response  =  requests .post (
192238                f"{ ENVOY_URL } { OPENAI_ENDPOINT }  " ,
193239                headers = headers ,
194240                json = payload ,
195241                timeout = 60 ,
196242            )
197243
198-             response_json  =  response .json ()
199-             results [test_case ["name" ]] =  response_json .get ("model" , "unknown" )
244+             # ExtProc should either process successfully or fail gracefully without hanging 
245+             passed  =  (
246+                 response .status_code  <  500 
247+             )  # No server errors due to ExtProc issues 
200248
201249            self .print_response_info (
202250                response ,
203251                {
204-                     "Category" : test_case ["category" ],
205-                     "Original Model" : DEFAULT_MODEL ,
206-                     "Routed Model" : results [test_case ["name" ]],
252+                     "Status Code" : response .status_code ,
253+                     "Error Handling" : "Graceful"  if  passed  else  "Server Error" ,
207254                },
208255            )
209256
210-              passed   =  ( 
211-                  response . status_code   <   400   and   results [ test_case [ "name" ]]  !=   "unknown" 
212-             ) 
213-             self .print_test_result (
214-                 passed = passed ,
215-                 message = ( 
216-                     f"Successfully routed to model:  { results [ test_case [ 'name' ]] } " 
217-                     if   passed 
218-                     else   f"Routing failed or returned unknown model" 
219-                 ) ,
257+         except  ( requests . exceptions . ConnectionError ,  requests . exceptions . Timeout )  as   e : 
258+             # Connection errors are acceptable - it shows the system is protecting itself 
259+             passed   =   True 
260+             self .print_response_info (
261+                 None ,
262+                 { 
263+                     "Connection" :  "Terminated (Expected)" , 
264+                     "Error Handling" :  "Protective disconnection" , 
265+                     "Error" :  str ( e )[: 100 ]  +   "..."   if   len ( str ( e ))  >   100   else   str ( e ), 
266+                 } ,
220267            )
221268
222-             self .assertLess (
223-                 response .status_code ,
224-                 400 ,
225-                 f"{ test_case ['name' ]}   request failed with status { response .status_code }  " ,
226-             )
269+         self .print_test_result (
270+             passed = passed ,
271+             message = (
272+                 "ExtProc handled error conditions gracefully" 
273+                 if  passed 
274+                 else  "ExtProc error handling failed" 
275+             ),
276+         )
227277
228-         # Final summary of routing results  
229-         if   len ( results )  ==   2 : 
230-             print ( " \n Routing Summary:" ) 
231-             print ( f"Math Query →  { results [ 'Math Query' ] } " ) 
232-              print ( f"Creative Writing Query →  { results [ 'Creative Writing Query' ] } "  )
278+         # The test passes if either the request succeeds or fails gracefully  
279+         self . assertTrue ( 
280+             passed , 
281+             "ExtProc should handle malformed input gracefully" , 
282+         )
233283
234-     def  test_extproc_body_modification (self ):
235-         """Test that the  ExtProc can modify the  request and response bodies .""" 
284+     def  test_extproc_performance_impact (self ):
285+         """Test that ExtProc doesn't significantly impact  request performance .""" 
236286        self .print_test_header (
237-             "ExtProc Body Modification  Test" ,
238-             "Verifies that ExtProc can modify request and response bodies while preserving essential fields " ,
287+             "ExtProc Performance Impact  Test" ,
288+             "Verifies that ExtProc processing doesn't add excessive latency " ,
239289        )
240290
291+         # Generate unique content for cache bypass 
241292        trace_id  =  str (uuid .uuid4 ())
242293
243294        payload  =  {
244295            "model" : DEFAULT_MODEL ,
245296            "messages" : [
246-                 {"role" : "assistant" , "content" : "You are a helpful assistant." },
247-                 {"role" : "user" , "content" : "What is quantum computing?" },
297+                 {"role" : "system" , "content" : "You are a helpful assistant." },
298+                 {
299+                     "role" : "user" ,
300+                     "content" : f"ExtProc performance test { trace_id [:8 ]}   - what is artificial intelligence?" ,
301+                 },
248302            ],
249-             "temperature" : 0.7 ,
250-             "test_field" : "should_be_preserved" ,
251303        }
252304
253-         headers  =  {
305+         # Test with minimal ExtProc processing 
306+         headers_minimal  =  {"Content-Type" : "application/json" }
307+ 
308+         # Test with ExtProc headers 
309+         headers_extproc  =  {
254310            "Content-Type" : "application/json" ,
255-             "X-Test-Trace-ID " : trace_id ,
256-             "X-Test-Body-Modification " : "true " ,
311+             "X-Test-Performance " : "true" ,
312+             "X-Processing-Mode " : "full " ,
257313        }
258314
259315        self .print_request_info (
260316            payload = payload ,
261-             expectations = "Expect: Request processing  with body modifications while preserving essential fields " ,
317+             expectations = "Expect: Reasonable response times  with ExtProc processing " ,
262318        )
263319
320+         import  time 
321+ 
322+         # Measure response time with ExtProc 
323+         start_time  =  time .time ()
264324        response  =  requests .post (
265-             f"{ ENVOY_URL } { OPENAI_ENDPOINT }  " , headers = headers , json = payload , timeout = 60 
325+             f"{ ENVOY_URL } { OPENAI_ENDPOINT }  " ,
326+             headers = headers_extproc ,
327+             json = payload ,
328+             timeout = 60 ,
266329        )
330+         response_time  =  time .time () -  start_time 
331+ 
332+         passed  =  (
333+             response .status_code  <  400  and  response_time  <  30.0 
334+         )  # Reasonable timeout 
267335
268-         response_json  =  response .json ()
269336        self .print_response_info (
270337            response ,
271338            {
272-                 "Original Model" : DEFAULT_MODEL ,
273-                 "Final Model" : response_json .get ("model" , "Not specified" ),
274-                 "Test Field Preserved" : "test_field"  in  response_json ,
339+                 "Response Time" : f"{ response_time :.2f}  s" ,
340+                 "Performance" : (
341+                     "Acceptable"  if  response_time  <  10.0  else  "Slow but functional" 
342+                 ),
275343            },
276344        )
277345
278-         passed  =  response .status_code  <  400  and  "model"  in  response_json 
279346        self .print_test_result (
280347            passed = passed ,
281348            message = (
282-                 "Request processed successfully with body modifications "
349+                 f"ExtProc processing completed in  { response_time :.2f } s "
283350                if  passed 
284-                 else  "Issues with request  processing or body modifications "
351+                 else  f"ExtProc  processing too slow:  { response_time :.2f } s "
285352            ),
286353        )
287354
288355        self .assertLess (
289356            response .status_code ,
290357            400 ,
291-             f"Request was rejected with status code { response .status_code }  " ,
358+             "ExtProc should not cause request failures" ,
359+         )
360+         self .assertLess (
361+             response_time ,
362+             30.0 ,
363+             "ExtProc should not cause excessive delays" ,
292364        )
293365
294366
0 commit comments