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