@@ -34,6 +34,7 @@ def setUpClass(cls):
3434 cls .gate = cls .steps [1 ]
3535 cls .extensions = next (step for step in cls .steps
3636 if step ["name" ] == "Install and verify the Foundry extension" )
37+ cls .responses = next (step for step in cls .steps if step .get ("id" ) == "responses" )
3738 cls .invocations = next (step for step in cls .steps if step .get ("id" ) == "invocations" )
3839
3940 def run_workflow_step (self , step , stub , env ):
@@ -107,6 +108,71 @@ def run_extension_setup(self, inventory, *, install_exit=0, list_exit=0):
107108 "LIST_EXIT" : str (list_exit ),
108109 })
109110
111+ def responses_sse (self , reply = "ISSUELENS_DEPLOYMENT_OK" ):
112+ event = {
113+ "type" : "response.completed" ,
114+ "response" : {
115+ "status" : "completed" , "error" : None , "incomplete_details" : None ,
116+ "output" : [{
117+ "type" : "message" , "role" : "assistant" , "status" : "completed" ,
118+ "content" : [{"type" : "output_text" , "text" : reply }],
119+ }],
120+ },
121+ }
122+ return f"event: response.completed\n data: { json .dumps (event )} \n \n data: [DONE]\n \n "
123+
124+ def run_responses_smoke (self , stream , * , invocation_exit = 0 ):
125+ stub = """
126+ azd() {
127+ if [[ "$*" != "ai agent invoke IssueLens $AGENT_TEST_PROMPT --environment $AZD_ENV_NAME --protocol responses --version $AGENT_VERSION --new-session --new-conversation --timeout 300 --output raw --no-prompt" ]]; then
128+ return 99
129+ fi
130+ printf '%s' "$RESPONSES_SSE"
131+ return "$RESPONSES_EXIT"
132+ }
133+ """
134+ if os .name == "nt" :
135+ # Git Bash cannot set RLIMIT_FSIZE; Linux CI exercises the real limit.
136+ stub += '\n ulimit() { [[ "$*" == "-f 8192" ]]; }\n '
137+ with tempfile .TemporaryDirectory () as directory :
138+ return self .run_workflow_step (self .responses , stub , self .workflow ["env" ] | {
139+ "RUNNER_TEMP" : pathlib .Path (directory ).as_posix (),
140+ "AGENT_VERSION" : "40" ,
141+ "RESPONSES_SSE" : stream ,
142+ "RESPONSES_EXIT" : str (invocation_exit ),
143+ })
144+
145+ def test_responses_accept_exact_completed_reply (self ):
146+ result = self .run_responses_smoke (self .responses_sse ())
147+ self .assertEqual (result .returncode , 0 , result .stdout + result .stderr )
148+ self .assertEqual (result .stdout , "" )
149+
150+ def test_responses_reject_invalid_streams_with_extended_timeout (self ):
151+ valid = self .responses_sse ()
152+ cases = {
153+ "empty" : "" ,
154+ "CLI heading only" : "Invoking agent IssueLens\n " ,
155+ "wrong reply" : self .responses_sse ("wrong" ),
156+ "extra whitespace" : self .responses_sse ("ISSUELENS_DEPLOYMENT_OK\n " ),
157+ "cancelled" : valid .replace ('"status": "completed"' , '"status": "cancelled"' ),
158+ "missing completion" : "event: response.in_progress\n data: {}\n \n " ,
159+ "duplicate completion" : valid + valid ,
160+ "malformed JSON" : valid + "data: not JSON\n \n " ,
161+ "error after reply" : valid + 'data: {"type":"error","message":"synthetic-error"}\n \n ' ,
162+ }
163+ for name , stream in cases .items ():
164+ with self .subTest (case = name ):
165+ result = self .run_responses_smoke (stream )
166+ self .assertNotEqual (result .returncode , 0 , result .stdout + result .stderr )
167+ self .assertIn ("::error::Responses did not complete" , result .stdout )
168+ self .assertNotIn ("synthetic-error" , result .stdout + result .stderr )
169+
170+ def test_responses_reject_nonzero_azd_exit_even_with_valid_output (self ):
171+ result = self .run_responses_smoke (self .responses_sse (), invocation_exit = 9 )
172+ self .assertNotEqual (result .returncode , 0 , result .stdout + result .stderr )
173+ self .assertIn ("::error::Responses invocation failed" , result .stdout )
174+ self .assertNotIn ("::error::Responses did not complete" , result .stdout )
175+
110176 def invocation_events (self ):
111177 return [
112178 {
@@ -129,7 +195,7 @@ def invocations_sse(self, events):
129195 def run_invocations_smoke (self , stream , * , invocation_exit = 0 ):
130196 stub = """
131197azd() {
132- if [[ "$*" != "ai agent invoke IssueLens --environment $AZD_ENV_NAME --protocol invocations --input-file $RUNNER_TEMP/issuelens-payload.json --version $AGENT_VERSION --new-session --timeout 120 --output raw --no-prompt" ]]; then
198+ if [[ "$*" != "ai agent invoke IssueLens --environment $AZD_ENV_NAME --protocol invocations --input-file $RUNNER_TEMP/issuelens-payload.json --version $AGENT_VERSION --new-session --timeout 300 --output raw --no-prompt" ]]; then
133199 return 99
134200 fi
135201 jq -e --arg input "$AGENT_TEST_PROMPT" '. == {input: $input}' "$RUNNER_TEMP/issuelens-payload.json" >/dev/null || return 98
@@ -529,14 +595,15 @@ def test_both_protocols_check_the_new_version_and_expected_reply(self):
529595 self .assertIn ('.version == $version and .status == "active"' , status ["run" ])
530596 for protocol in ("responses" , "invocations" ):
531597 step = next (step for step in self .steps if step .get ("id" ) == protocol )
532- self .assertEqual (step ["timeout-minutes" ], "3 " )
598+ self .assertEqual (step ["timeout-minutes" ], "6 " )
533599 self .assertEqual (step ["env" ], {"AGENT_VERSION" : "${{ steps.status.outputs.version }}" })
534600 for text in (
535601 f"--protocol { protocol } " , '--version "$AGENT_VERSION"' , "--new-session" ,
536- "--timeout 120 " , "--output raw" , "ulimit -f 8192" ,
602+ "--timeout 300 " , "--output raw" , "ulimit -f 8192" ,
537603 'jq -e -s --arg expected "$EXPECTED_REPLY"' , "exit 1" ,
538604 ):
539605 self .assertIn (text , step ["run" ])
606+ self .assertEqual (step ["run" ].count ("azd ai agent invoke" ), 1 )
540607 self .assertIn ('--arg input "$AGENT_TEST_PROMPT" \' {input: $input}\' ' , self .commands )
541608 self .assertIn ("--new-conversation" , self .commands )
542609 self .assertIn ('select(.type == "response.completed")' , self .commands )
@@ -583,6 +650,8 @@ def test_setup_guide_documents_the_official_guide_and_all_configuration(self):
583650 "1.34.2" , "1.0.0-beta.2" ,
584651 "No automatic retry or rollback" , "No live deployment" ,
585652 "installedVersion" , "--no-dependencies" , "Bash and jq" ,
653+ "each invocation to 300 seconds within a 6-minute step" ,
654+ "does not add workflow-level retries" ,
586655 ):
587656 self .assertIn (text , guide )
588657 for name , version in FOUNDRY_EXTENSIONS .items ():
0 commit comments