Files
codex/codex-rs/exec-server/tests/unit/client_provisioning_tests.rs
Anton Panasenko e6249b5296 Recover deferred environments after provisioning failure (#42388)
## What changed

- Allow a valid Ready report to replace a deferred environment's provisioning failure while preserving the same environment instance.
- Treat provisioning failures as retryable and reconnect when a completed or concurrently publishing startup attempt contains a stale failure.
- Report provisioning failures from `status()` without starting a connection, and retain Ready state when a late failure arrives.

## Testing

- Cover recovery after failed and invalid provisioning reports, capability-root resolution after recovery, and the race between Ready and publication of an earlier startup failure.

GitOrigin-RevId: cce419403248168da81bc5a6ab20f919aed81142
2026-09-02 23:49:29 +00:00

77 lines
2.9 KiB
Rust

//! Exercises provisioning recovery while startup is publishing its previous failure.
use super::*;
use crate::client_api::Deferred;
use crate::noise_channel::NoiseChannelIdentity;
use futures::poll;
use tokio::sync::oneshot;
#[test_case::test_case(false; "initial startup")]
#[test_case::test_case(true; "subsequent attempt")]
#[tokio::test]
async fn caller_after_ready_retries_an_unpublished_provisioning_failure(reconnecting: bool) {
let (readiness, readiness_rx) = watch::channel(Some(Err("first failure".to_string())));
let client = LazyRemoteExecServerClient::new(
ExecServerTransportParams::Deferred(Box::new(Deferred {
readiness: readiness_rx,
transport: ExecServerTransportParams::NoiseRendezvous {
provider: Arc::new(FailingProvider),
identity: NoiseChannelIdentity::generate().expect("Noise identity"),
},
})),
HttpClientFactory::new(codex_http_client::OutboundProxyPolicy::ReqwestDefault),
);
let (failed_tx, failed_rx) = oneshot::channel();
let (publish_tx, publish_rx) = oneshot::channel();
let attempt = if reconnecting {
assert!(client.wait_until_ready().await.is_err());
let attempt = Arc::new(ConnectionAttempt::default());
*client.reconnect.lock().expect("reconnect lock") = Some(Arc::clone(&attempt));
attempt
} else {
Arc::clone(&client.startup)
};
let startup_client = client.clone();
let startup = tokio::spawn(async move {
attempt
.result
.get_or_init(|| async {
let result = startup_client.connect_once(&attempt).await;
assert!(result.is_err());
failed_tx.send(()).expect("failure observed");
// Suspend at the publication boundary, as another executor thread could.
publish_rx.await.expect("publish startup result");
result
})
.await
.clone()
});
failed_rx.await.expect("startup consumed the Failed report");
readiness.send_replace(Some(Ok(())));
let mut after_ready = Box::pin(client.wait_until_ready());
assert!(poll!(&mut after_ready).is_pending());
publish_tx.send(()).expect("release startup");
let error = after_ready.await.unwrap_err();
assert!(
error.to_string().contains("provider reached after Ready"),
"post-Ready caller should retry the stale failure: {error}"
);
assert!(startup.await.expect("startup task").is_err());
}
struct FailingProvider;
impl crate::NoiseRendezvousConnectProvider for FailingProvider {
fn connect_bundle(
&self,
_: crate::NoiseChannelPublicKey,
) -> BoxFuture<'_, Result<crate::NoiseRendezvousConnectBundle, ExecServerError>> {
Box::pin(async {
Err(ExecServerError::Protocol(
"provider reached after Ready".to_string(),
))
})
}
}