@@ -59,6 +59,11 @@ def echo_agent_dir(self):
5959 """Get the path to the echo agent directory."""
6060 return Path (__file__ ).parent .parent / "example_agents" / "echo_agent"
6161
62+ @pytest .fixture
63+ def streaming_echo_agent_dir (self ):
64+ """Get the path to the streaming echo agent directory."""
65+ return Path (__file__ ).parent .parent / "example_agents" / "streaming_echo_agent"
66+
6267 @pytest .fixture
6368 def setup_agent_in_temp (self , echo_agent_dir ):
6469 """
@@ -86,6 +91,33 @@ def setup_agent_in_temp(self, echo_agent_dir):
8691
8792 yield temp_path
8893
94+ @pytest .fixture
95+ def setup_streaming_agent_in_temp (self , streaming_echo_agent_dir ):
96+ """
97+ Setup a temporary directory with the streaming echo agent and proper configuration.
98+ Yields the temp directory path and cleans up after.
99+ """
100+ with tempfile .TemporaryDirectory () as temp_dir :
101+ temp_path = Path (temp_dir )
102+
103+ # Copy the streaming echo agent main.py
104+ shutil .copy (streaming_echo_agent_dir / "main.py" , temp_path / "main.py" )
105+
106+ # Create .gradient directory and config
107+ gradient_dir = temp_path / ".gradient"
108+ gradient_dir .mkdir ()
109+
110+ config = {
111+ "agent_name" : "test-streaming-echo-agent" ,
112+ "agent_environment" : "main" ,
113+ "entrypoint_file" : "main.py" ,
114+ }
115+
116+ with open (gradient_dir / "agent.yml" , "w" ) as f :
117+ yaml .safe_dump (config , f )
118+
119+ yield temp_path
120+
89121 @pytest .mark .cli
90122 def test_agent_run_happy_path (self , setup_agent_in_temp ):
91123 """
@@ -386,5 +418,135 @@ def test_agent_run_run_endpoint_with_various_inputs(self, setup_agent_in_temp):
386418 assert data ["echo" ] == "Hello `} E1-('"
387419 logger .info ("Unicode test passed" )
388420
421+ finally :
422+ cleanup_process (process )
423+
424+ @pytest .mark .cli
425+ def test_streaming_agent_without_evaluation_id_streams_response (
426+ self , setup_streaming_agent_in_temp
427+ ):
428+ """
429+ Test that a streaming agent returns a streamed response when no evaluation-id header is sent.
430+ Verifies:
431+ - Response is streamed (text/event-stream content type)
432+ - Response contains the expected content
433+ """
434+ logger = logging .getLogger (__name__ )
435+ temp_dir = setup_streaming_agent_in_temp
436+ port = find_free_port ()
437+ process = None
438+
439+ try :
440+ logger .info (f"Starting streaming agent on port { port } in { temp_dir } " )
441+
442+ # Start the agent server
443+ process = subprocess .Popen (
444+ [
445+ "gradient" ,
446+ "agent" ,
447+ "run" ,
448+ "--port" ,
449+ str (port ),
450+ "--no-dev" ,
451+ ],
452+ cwd = temp_dir ,
453+ start_new_session = True ,
454+ )
455+
456+ # Wait for server to be ready
457+ server_ready = wait_for_server (port , timeout = 30 )
458+ assert server_ready , "Server did not start within timeout"
459+
460+ # Make a streaming request WITHOUT evaluation-id header
461+ with requests .post (
462+ f"http://localhost:{ port } /run" ,
463+ json = {"prompt" : "Hello, World!" },
464+ stream = True ,
465+ timeout = 30 ,
466+ ) as response :
467+ assert response .status_code == 200
468+
469+ # Verify it's a streaming response (text/event-stream)
470+ content_type = response .headers .get ("content-type" , "" )
471+ assert "text/event-stream" in content_type , (
472+ f"Expected text/event-stream content type for streaming, got: { content_type } "
473+ )
474+
475+ # Collect chunks to verify content
476+ chunks = list (response .iter_content (decode_unicode = True ))
477+ full_content = "" .join (c for c in chunks if c )
478+
479+ # Verify the content contains the expected streamed output
480+ assert "Echo:" in full_content or "Hello, World!" in full_content , (
481+ f"Expected streamed content to contain prompt, got: { full_content } "
482+ )
483+
484+ logger .info (f"Streaming response received with { len (chunks )} chunks" )
485+ logger .info (f"Full content: { full_content } " )
486+
487+ finally :
488+ cleanup_process (process )
489+
490+ @pytest .mark .cli
491+ def test_streaming_agent_with_evaluation_id_returns_single_response (
492+ self , setup_streaming_agent_in_temp
493+ ):
494+ """
495+ Test that a streaming agent returns a single JSON response (not streamed)
496+ when the evaluation-id header is present.
497+ Verifies:
498+ - Response is NOT streamed (application/json content type)
499+ - Response contains the complete collected content
500+ """
501+ logger = logging .getLogger (__name__ )
502+ temp_dir = setup_streaming_agent_in_temp
503+ port = find_free_port ()
504+ process = None
505+
506+ try :
507+ logger .info (f"Starting streaming agent on port { port } in { temp_dir } " )
508+
509+ # Start the agent server
510+ process = subprocess .Popen (
511+ [
512+ "gradient" ,
513+ "agent" ,
514+ "run" ,
515+ "--port" ,
516+ str (port ),
517+ "--no-dev" ,
518+ ],
519+ cwd = temp_dir ,
520+ start_new_session = True ,
521+ )
522+
523+ # Wait for server to be ready
524+ server_ready = wait_for_server (port , timeout = 30 )
525+ assert server_ready , "Server did not start within timeout"
526+
527+ # Make a request WITH evaluation-id header
528+ response = requests .post (
529+ f"http://localhost:{ port } /run" ,
530+ json = {"prompt" : "Hello, World!" },
531+ headers = {"evaluation-id" : "test-eval-123" },
532+ timeout = 30 ,
533+ )
534+ assert response .status_code == 200
535+
536+ # Verify it's NOT a streaming response (should be application/json)
537+ content_type = response .headers .get ("content-type" , "" )
538+ assert "application/json" in content_type , (
539+ f"Expected application/json content type for evaluation mode, got: { content_type } "
540+ )
541+
542+ # Verify the response contains the complete content
543+ result = response .json ()
544+ expected_content = "Echo: Hello, World! [DONE]"
545+ assert result == expected_content , (
546+ f"Expected complete collected content '{ expected_content } ', got: { result } "
547+ )
548+
549+ logger .info (f"Single JSON response received: { result } " )
550+
389551 finally :
390552 cleanup_process (process )
0 commit comments