Skip to content

Commit c431d42

Browse files
chagongCopilot
andauthored
Allow Foundry smoke tests to finish model retries (#59)
Extend both protocol invocation deadlines to 300 seconds within six-minute steps. Preserve deployment gates and strict reply validation, cover Responses execution failures, and document the bounded retry headroom. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent f532e9c commit c431d42

3 files changed

Lines changed: 83 additions & 10 deletions

File tree

‎.github/workflows/deploy-foundry.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,14 @@ jobs:
143143
echo "version=$version" >>"$GITHUB_OUTPUT"
144144
- name: Smoke-test responses
145145
id: responses
146-
timeout-minutes: 3
146+
timeout-minutes: 6
147147
env:
148148
AGENT_VERSION: ${{ steps.status.outputs.version }}
149149
run: |
150150
ulimit -f 8192
151151
azd ai agent invoke IssueLens "$AGENT_TEST_PROMPT" --environment "$AZD_ENV_NAME" \
152152
--protocol responses --version "$AGENT_VERSION" --new-session --new-conversation \
153-
--timeout 120 --output raw --no-prompt >"$RUNNER_TEMP/issuelens-responses.txt" 2>"$RUNNER_TEMP/issuelens-smoke-errors.txt" ||
153+
--timeout 300 --output raw --no-prompt >"$RUNNER_TEMP/issuelens-responses.txt" 2>"$RUNNER_TEMP/issuelens-smoke-errors.txt" ||
154154
{ echo "::error::Responses invocation failed; inspect Foundry before retrying."; exit 1; }
155155
sed -n 's/^data: *//p' "$RUNNER_TEMP/issuelens-responses.txt" | grep -v '^\[DONE\]' |
156156
jq -e -s --arg expected "$EXPECTED_REPLY" '
@@ -165,15 +165,15 @@ jobs:
165165
{ echo "::error::Responses did not complete with the expected smoke reply."; exit 1; }
166166
- name: Smoke-test invocations
167167
id: invocations
168-
timeout-minutes: 3
168+
timeout-minutes: 6
169169
env:
170170
AGENT_VERSION: ${{ steps.status.outputs.version }}
171171
run: |
172172
ulimit -f 8192
173173
jq -n --arg input "$AGENT_TEST_PROMPT" '{input: $input}' >"$RUNNER_TEMP/issuelens-payload.json"
174174
azd ai agent invoke IssueLens --environment "$AZD_ENV_NAME" --protocol invocations \
175175
--input-file "$RUNNER_TEMP/issuelens-payload.json" --version "$AGENT_VERSION" --new-session \
176-
--timeout 120 --output raw --no-prompt >"$RUNNER_TEMP/issuelens-invocations.txt" 2>"$RUNNER_TEMP/issuelens-smoke-errors.txt" ||
176+
--timeout 300 --output raw --no-prompt >"$RUNNER_TEMP/issuelens-invocations.txt" 2>"$RUNNER_TEMP/issuelens-smoke-errors.txt" ||
177177
{ echo "::error::Invocations request failed; inspect Foundry before retrying."; exit 1; }
178178
sed -n 's/^data: *//p' "$RUNNER_TEMP/issuelens-invocations.txt" |
179179
jq -e -s --arg expected "$EXPECTED_REPLY" '

‎docs/guide.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,9 +1162,13 @@ or null. Its available-tools inventory is not tool execution; tool requests,
11621162
tool events, and all other sub-agent events still fail the smoke check.
11631163

11641164
The job is limited to 40 minutes, the native deployment wait to 20 minutes, and
1165-
each invocation to 120 seconds within a 3-minute step. The summary records the
1166-
commit, logical GitHub environment, version, readiness, and protocol outcomes,
1167-
not Azure identifiers or endpoints. Azure CLI account output is disabled;
1165+
each invocation to 300 seconds within a 6-minute step. This gives the SDK's
1166+
existing model retries time to complete after transient Foundry capacity or
1167+
throttling errors, with a minute of step headroom for CLI setup and response
1168+
validation. It does not add workflow-level retries or guarantee model capacity;
1169+
timeouts, invocation failures, and invalid replies still fail the job.
1170+
The summary records the commit, logical GitHub environment, version, readiness,
1171+
and protocol outcomes, not Azure identifiers or endpoints. Azure CLI account output is disabled;
11681172
azd configuration/deployment output, readiness details, and raw agent responses
11691173
stay in runner-local files, including on failure. The workflow never prints
11701174
`azd env get-values` or uploads these files. Temporary files and `.azure` are

‎tests/test_deploy_foundry_workflow.py‎

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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\ndata: {json.dumps(event)}\n\ndata: [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 += '\nulimit() { [[ "$*" == "-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\ndata: {}\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 = """
131197
azd() {
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

Comments
 (0)