From e76ce0b67f5dc276887fbf2b76e4f4db158d74af Mon Sep 17 00:00:00 2001 From: trnorga <328204342+trnorga@users.noreply.github.com> Date: Fri, 11 Sep 2026 19:20:40 -0700 Subject: [PATCH] test(agent): cover the codex iteration-summary retry path Iteration-limit summaries retry once when the first attempt comes back empty. Both attempts share one builder closure, so assert the retry body is as tool-free as the first: forces an empty summary, then checks every captured codex request for tools / tool_choice / parallel_tool_calls. Addresses the retry-coverage request on #32777. --- tests/run_agent/test_run_agent.py | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index a470bf990b..9e000fc011 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -2947,6 +2947,58 @@ class TestHandleMaxIterations: assert "tool_choice" not in captured assert "parallel_tool_calls" not in captured + def test_codex_summary_retry_also_strips_tool_controls(self, agent): + """The retry attempt must be as toolless as the first one. + + Iteration-limit summaries retry once when the first summary comes back empty; both + attempts build their body through the same codex path, so both must drop the tool-control + block the transport emits alongside ``tools`` (agent/transports/codex.py). + """ + agent.api_mode = "codex_responses" + agent.provider = "openai-codex" + agent.base_url = "https://chatgpt.com/backend-api/codex" + agent._base_url_lower = agent.base_url.lower() + agent._base_url_hostname = "chatgpt.com" + agent.model = "gpt-5.5" + agent._cached_system_prompt = "You are helpful." + + bodies = [] + tool_laden = { + "model": "gpt-5.5", + "input": [{"role": "user", "content": "do stuff"}], + "tools": [{"type": "function", "name": "web_search"}], + "tool_choice": "auto", + "parallel_tool_calls": True, + } + + def fake_run_codex_stream(kwargs): + bodies.append(dict(kwargs)) + # First attempt returns an empty summary so the caller takes the retry path. + text = "" if len(bodies) == 1 else "Summary" + return SimpleNamespace( + status="completed", + output=[ + SimpleNamespace( + type="message", + status="completed", + content=[SimpleNamespace(type="output_text", text=text)], + ) + ], + ) + + with ( + patch.object(agent, "_build_api_kwargs", return_value=tool_laden.copy()), + patch.object(agent, "_run_codex_stream", side_effect=fake_run_codex_stream), + ): + result = agent._handle_max_iterations([{"role": "user", "content": "do stuff"}], 90) + + assert result == "Summary" + assert len(bodies) == 2, f"expected one retry after the empty summary, got {len(bodies)} attempts" + for attempt_index, sent in enumerate(bodies): + assert "tools" not in sent, f"attempt {attempt_index}: tools leaked into the summary call" + assert "tool_choice" not in sent, f"attempt {attempt_index}: tool_choice leaked" + assert "parallel_tool_calls" not in sent, f"attempt {attempt_index}: parallel_tool_calls leaked" + def test_api_sanitizer_matches_responses_call_id_when_id_differs(self, agent): messages = [ {