Stabilize the Guardian network review timeout test (#54082)

## Why

Advancing the test clock expires the review, but command cancellation and output drainage still run asynchronously. The model can poll before the final timeout failure is available.

## What changed

Hold the initial tool result until the command emits `ExecCommandEnd`, with a bounded wait that also catches unexpected approval requests or turn completion. Only request a poll when the initial result reports a running process, and accept the timeout output from either `exec_command` or `write_stdin`.

Keep the assertions that the model receives the timeout explanation without a user approval prompt or a misleading user-rejection message.

GitOrigin-RevId: 219a1579dce35caf8f0d45b1d92a7343ec1a8935
This commit is contained in:
felixxia-oai
2026-10-14 20:06:28 +00:00
committed by copyberry
parent 01ff831ce9
commit 8094365e6a
+59 -8
View File
@@ -14,6 +14,10 @@ use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
use codex_exec_server::RemoveOptions;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ToolFinishInput;
use codex_extension_api::ToolLifecycleContributor;
use codex_extension_api::ToolLifecycleFuture;
use codex_features::Feature;
use codex_history::RolloutItem;
#[cfg(unix)]
@@ -665,10 +669,34 @@ async fn timed_out_guardian_network_review_uses_timeout_outcome_without_user_fal
skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(()));
let server = start_mock_server().await;
let test = managed_network_unified_exec_test(&server).await?;
struct HoldExecResult {
call_id: &'static str,
release: tokio::sync::Notify,
}
impl ToolLifecycleContributor for HoldExecResult {
fn on_tool_finish<'a>(&'a self, input: ToolFinishInput<'a>) -> ToolLifecycleFuture<'a> {
Box::pin(async move {
if input.call_id == self.call_id {
self.release.notified().await;
}
})
}
}
let call_id = "guardian-network-timeout";
let poll_call_id = "guardian-network-timeout-poll";
let hold_result = Arc::new(HoldExecResult {
call_id,
release: tokio::sync::Notify::new(),
});
let mut extensions = ExtensionRegistryBuilder::new();
extensions.tool_lifecycle_contributor(hold_result.clone());
let server = start_mock_server().await;
let test = managed_network_unified_exec_builder(&[])?
.with_extensions(Arc::new(extensions.build()))
.build_with_remote_and_local_env(&server)
.await?;
mount_sse_once_match(
&server,
|request: &wiremock::Request| {
@@ -700,7 +728,9 @@ async fn timed_out_guardian_network_review_uses_timeout_outcome_without_user_fal
mount_sse_once_match(
&server,
|request: &wiremock::Request| {
!is_guardian_request(request) && request_body_contains(request, call_id)
!is_guardian_request(request)
&& request_body_contains(request, call_id)
&& request_body_contains(request, "Process running with session ID 1000")
},
sse(vec![
ev_response_created("resp-guardian-timeout-parent-followup"),
@@ -720,7 +750,7 @@ async fn timed_out_guardian_network_review_uses_timeout_outcome_without_user_fal
let parent_final = mount_sse_once_match(
&server,
|request: &wiremock::Request| {
!is_guardian_request(request) && request_body_contains(request, poll_call_id)
!is_guardian_request(request) && request_body_contains(request, call_id)
},
sse(vec![
ev_response_created("resp-guardian-timeout-parent-final"),
@@ -742,12 +772,33 @@ async fn timed_out_guardian_network_review_uses_timeout_outcome_without_user_fal
tokio::time::pause();
tokio::time::advance(Duration::from_secs(91)).await;
tokio::time::resume();
// The clock jump expires the review, but cancellation and output drainage
// still run asynchronously. Hold the tool result until its terminal event
// so the model cannot poll before the final failure is available.
let completion = tokio::time::timeout(
Duration::from_secs(/*secs*/ 10),
wait_for_event(&test.codex, |event| match event {
EventMsg::ExecCommandEnd(event) => event.call_id == call_id,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) => true,
_ => false,
}),
)
.await;
// notify_one retains a permit if the tool-finish callback has not run yet.
hold_result.release.notify_one();
let completion = completion.context("timed out waiting for the network command to finish")?;
assert!(
matches!(completion, EventMsg::ExecCommandEnd(_)),
"unexpected event before command completion: {completion:?}"
);
wait_for_completion_without_network_prompt(&test).await;
let tool_output = parent_final
.requests()
.iter()
.find_map(|request| request.function_call_output_text(poll_call_id))
let request = parent_final.single_request();
// If the initial exec call already returned its final error, no poll is needed.
let tool_output = request
.function_call_output_text(poll_call_id)
.or_else(|| request.function_call_output_text(call_id))
.context("expected timed-out Guardian tool output")?;
assert!(
tool_output.contains(concat!(