10
10
import json
11
11
import os
12
12
import sys
13
+ import unittest
13
14
import uuid
14
15
15
16
import requests
16
17
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
20
20
21
21
# Constants
22
22
ENVOY_URL = "http://localhost:8801"
23
23
OPENAI_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
25
25
26
26
27
27
class EnvoyExtProcTest (SemanticRouterTestBase ):
@@ -35,11 +35,13 @@ def setUp(self):
35
35
)
36
36
37
37
try :
38
+ # Use unique content to bypass cache for setup check
39
+ setup_id = str (uuid .uuid4 ())[:8 ]
38
40
payload = {
39
41
"model" : DEFAULT_MODEL ,
40
42
"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 } " },
43
45
],
44
46
}
45
47
@@ -77,8 +79,11 @@ def test_request_headers_propagation(self):
77
79
payload = {
78
80
"model" : DEFAULT_MODEL ,
79
81
"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
+ },
82
87
],
83
88
"temperature" : 0.7 ,
84
89
}
@@ -137,158 +142,225 @@ def test_request_headers_propagation(self):
137
142
)
138
143
self .assertIn ("model" , response_json , "Response is missing 'model' field" )
139
144
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 ."""
142
147
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 " ,
145
150
)
146
151
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 ,
153
185
{
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 ,
157
189
},
158
- ]
190
+ )
159
191
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
+ )
161
201
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
+ )
164
207
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
+ )
166
214
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
+ }
178
223
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
+ }
185
230
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
+ )
190
235
236
+ try :
191
237
response = requests .post (
192
238
f"{ ENVOY_URL } { OPENAI_ENDPOINT } " ,
193
239
headers = headers ,
194
240
json = payload ,
195
241
timeout = 60 ,
196
242
)
197
243
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
200
248
201
249
self .print_response_info (
202
250
response ,
203
251
{
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" ,
207
254
},
208
255
)
209
256
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
+ } ,
220
267
)
221
268
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
+ )
227
277
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
+ )
233
283
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 ."""
236
286
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 " ,
239
289
)
240
290
291
+ # Generate unique content for cache bypass
241
292
trace_id = str (uuid .uuid4 ())
242
293
243
294
payload = {
244
295
"model" : DEFAULT_MODEL ,
245
296
"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
+ },
248
302
],
249
- "temperature" : 0.7 ,
250
- "test_field" : "should_be_preserved" ,
251
303
}
252
304
253
- headers = {
305
+ # Test with minimal ExtProc processing
306
+ headers_minimal = {"Content-Type" : "application/json" }
307
+
308
+ # Test with ExtProc headers
309
+ headers_extproc = {
254
310
"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 " ,
257
313
}
258
314
259
315
self .print_request_info (
260
316
payload = payload ,
261
- expectations = "Expect: Request processing with body modifications while preserving essential fields " ,
317
+ expectations = "Expect: Reasonable response times with ExtProc processing " ,
262
318
)
263
319
320
+ import time
321
+
322
+ # Measure response time with ExtProc
323
+ start_time = time .time ()
264
324
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 ,
266
329
)
330
+ response_time = time .time () - start_time
331
+
332
+ passed = (
333
+ response .status_code < 400 and response_time < 30.0
334
+ ) # Reasonable timeout
267
335
268
- response_json = response .json ()
269
336
self .print_response_info (
270
337
response ,
271
338
{
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
+ ),
275
343
},
276
344
)
277
345
278
- passed = response .status_code < 400 and "model" in response_json
279
346
self .print_test_result (
280
347
passed = passed ,
281
348
message = (
282
- "Request processed successfully with body modifications "
349
+ f"ExtProc processing completed in { response_time :.2f } s "
283
350
if passed
284
- else "Issues with request processing or body modifications "
351
+ else f"ExtProc processing too slow: { response_time :.2f } s "
285
352
),
286
353
)
287
354
288
355
self .assertLess (
289
356
response .status_code ,
290
357
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" ,
292
364
)
293
365
294
366
0 commit comments