Upgrade rmcp to 3.5.1 and harden lifecycle tests (#52972)

## What changed

- Update the pinned `rmcp` dependency and Cargo/Bazel lockfiles, and migrate MCP handlers and fixtures to `ClientConfig` and `ServerConfig`.
- Stop requiring responses for cancelled elicitation requests in cancellation tests while retaining checks for cleanup and continued connection usability.
- Handle client disconnects during WebSocket writes in the TUI test server, use an asynchronous HTTP mock for loopback proxy testing, and improve remote executor test diagnostics and the Windows subprocess fixture.
- Use `invalid_prompt` for the memory worker failure fixture.

## Testing

Add a regression test covering transport EOF during both form elicitation and user verification. Keep the client alive while checking that pending response channels close, the timeout pause clears, and the service exits with `QuitReason::Closed`.

GitOrigin-RevId: c0166b83fab3b59d84a5207c6630cb6adcf3b230
This commit is contained in:
alishobeiri-oai
2026-10-10 19:43:24 +00:00
committed by copyberry
parent 34f33e4ada
commit de8fab6d7a
26 changed files with 212 additions and 162 deletions
+15 -13
View File
@@ -281,7 +281,7 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr
"2",
])
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true);
let mut child = command.spawn()?;
@@ -290,7 +290,9 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr
.take()
.ok_or_else(|| anyhow::anyhow!("remote exec-server stdin was not piped"))?;
let environment_websocket = accept_parent_lifetime_websocket(&listener, TEST_TIMEOUT).await?;
let environment_websocket = accept_parent_lifetime_websocket(&listener, TEST_TIMEOUT)
.await
.context("remote executor failed to attach")?;
// Remote startup must capture the version before registration, not on the first initialize.
std::fs::write(&manifest, r#"{"version":"9.9.9"}"#)?;
let executor_public_key = registered_parent_lifetime_executor_public_key(&registry).await?;
@@ -311,7 +313,9 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr
};
let client_task =
tokio::spawn(async move { ExecServerClient::connect_noise_rendezvous(harness_args).await });
let harness_websocket = accept_parent_lifetime_websocket(&listener, TEST_TIMEOUT).await?;
let harness_websocket = accept_parent_lifetime_websocket(&listener, TEST_TIMEOUT)
.await
.context("harness failed to attach")?;
let relay_task = tokio::spawn(proxy_parent_lifetime_relay(
environment_websocket,
harness_websocket,
@@ -342,9 +346,12 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr
#[cfg(windows)]
let argv = vec![
"cmd.exe",
"/C",
"if defined CODEX_EXEC_SERVER_EXIT_ON_STDIN_CLOSE (exit /b 1) else ping -n 61 127.0.0.1",
"powershell.exe",
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-Command",
"if (Test-Path Env:CODEX_EXEC_SERVER_EXIT_ON_STDIN_CLOSE) { exit 1 }; Start-Sleep -Seconds 60",
];
#[cfg(not(windows))]
let argv = vec![
@@ -385,15 +392,10 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr
drop(stdin);
let output = tokio::time::timeout(TEST_TIMEOUT, child.wait_with_output())
let status = tokio::time::timeout(TEST_TIMEOUT, child.wait())
.await
.map_err(|_| anyhow::anyhow!("remote exec-server did not exit after stdin closed"))??;
anyhow::ensure!(
output.status.success(),
"remote exec-server exited with {}; stderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
anyhow::ensure!(status.success(), "remote exec-server exited with {status}");
relay_task.abort();
let _ = relay_task.await;
+16 -47
View File
@@ -1,11 +1,14 @@
#![cfg(target_os = "linux")]
use std::net::TcpListener;
use std::time::Duration;
use std::time::Instant;
use anyhow::Result;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable";
@@ -71,50 +74,17 @@ mode = "full"
Ok(())
}
#[test]
fn sandbox_with_network_proxy_allows_explicit_loopback_access() -> Result<()> {
#[tokio::test]
async fn sandbox_with_network_proxy_allows_explicit_loopback_access() -> Result<()> {
let codex_home = TempDir::new()?;
let listener = TcpListener::bind("127.0.0.2:0")?;
let port = listener.local_addr()?.port();
listener.set_nonblocking(true)?;
let server = std::thread::spawn(move || -> std::io::Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match listener.accept() {
Ok((mut stream, _)) => {
// Closing with unread request bytes can reset the connection and yield a 502.
stream.set_nonblocking(false)?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut request = Vec::new();
let mut buffer = [0_u8; 1024];
while !request.windows(4).any(|window| window == b"\r\n\r\n") {
let bytes_read = std::io::Read::read(&mut stream, &mut buffer)?;
if bytes_read == 0 || request.len() + bytes_read > 64 * 1024 {
return Err(std::io::Error::other(
"incomplete or oversized loopback request headers",
));
}
request.extend_from_slice(&buffer[..bytes_read]);
}
std::io::Write::write_all(
&mut stream,
b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n",
)?;
return Ok(());
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
if Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"timed out waiting for allowlisted loopback request",
));
}
std::thread::sleep(Duration::from_millis(10));
}
Err(err) => return Err(err),
}
}
});
let server = MockServer::builder().listener(listener).start().await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
@@ -138,7 +108,7 @@ allow_local_binding = false
)?;
let url = format!("http://127.0.0.2:{port}/");
let output = std::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
let output = tokio::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
.env("CODEX_HOME", codex_home.path())
.args([
"sandbox",
@@ -155,7 +125,8 @@ allow_local_binding = false
"4",
url.as_str(),
])
.output()?;
.output()
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains(BWRAP_UNAVAILABLE_ERR) {
@@ -170,7 +141,5 @@ allow_local_binding = false
String::from_utf8_lossy(&output.stdout),
stderr,
);
server.join().expect("loopback server panicked")?;
Ok(())
}