mirror of
https://github.com/openai/codex.git
synced 2026-10-25 23:57:40 +00:00
## What changed - Add the experimental `environment/status` request for inspecting a configured environment without starting or reconnecting it. - Report `ready`, `pending`, `disconnected`, or `unknown`, including error details for disconnected and unknown environments. - Probe ready remote environments over their existing exec-server connection. ## Testing - Add an app-server integration test covering local and remote ready, pending, disconnected, and unknown environments. GitOrigin-RevId: 397bba603aa9e0b59008ee6cb7cbde46e357652c
217 lines
7.7 KiB
Rust
217 lines
7.7 KiB
Rust
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use app_test_support::TestAppServer;
|
|
use app_test_support::to_response;
|
|
use codex_app_server_protocol::EnvironmentAddParams;
|
|
use codex_app_server_protocol::EnvironmentAddResponse;
|
|
use codex_app_server_protocol::EnvironmentStatusKind;
|
|
use codex_app_server_protocol::EnvironmentStatusParams;
|
|
use codex_app_server_protocol::EnvironmentStatusResponse;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use futures::SinkExt;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use tokio::net::TcpListener;
|
|
use tokio::sync::oneshot;
|
|
use tokio::time::sleep;
|
|
use tokio::time::timeout;
|
|
use tokio_tungstenite::accept_async;
|
|
use tokio_tungstenite::tungstenite::Message;
|
|
|
|
use super::exec_server_test_support::accept_initialized_exec_server;
|
|
use super::exec_server_test_support::read_exec_server_json;
|
|
|
|
const RPC_TIMEOUT: Duration = Duration::from_secs(10);
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn environment_status_reports_connection_states_with_auto_env() -> Result<()> {
|
|
let ready_listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let ready_exec_server_url = format!("ws://{}", ready_listener.local_addr()?);
|
|
let (ready_connected_tx, ready_connected_rx) = oneshot::channel();
|
|
let (ready_release_tx, mut ready_release_rx) = oneshot::channel();
|
|
let ready_exec_server = tokio::spawn(async move {
|
|
let mut websocket = accept_initialized_exec_server(ready_listener).await?;
|
|
ready_connected_tx
|
|
.send(())
|
|
.map_err(|()| anyhow::anyhow!("status test stopped before exec-server was ready"))?;
|
|
|
|
loop {
|
|
tokio::select! {
|
|
_ = &mut ready_release_rx => return Ok::<_, anyhow::Error>(()),
|
|
request = read_exec_server_json(&mut websocket) => {
|
|
let request = request?;
|
|
assert_eq!(request["method"], "environment/status");
|
|
websocket
|
|
.send(Message::Text(
|
|
json!({
|
|
"id": request["id"],
|
|
"result": {"status": "ready"},
|
|
})
|
|
.to_string()
|
|
.into(),
|
|
))
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let pending_listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let pending_exec_server_url = format!("ws://{}", pending_listener.local_addr()?);
|
|
let (pending_connected_tx, pending_connected_rx) = oneshot::channel();
|
|
let (pending_release_tx, pending_release_rx) = oneshot::channel();
|
|
let pending_exec_server = tokio::spawn(async move {
|
|
let (stream, _) = pending_listener.accept().await?;
|
|
let _websocket = accept_async(stream).await?;
|
|
pending_connected_tx
|
|
.send(())
|
|
.map_err(|()| anyhow::anyhow!("status test stopped before pending connection"))?;
|
|
let _ = pending_release_rx.await;
|
|
Ok::<_, anyhow::Error>(())
|
|
});
|
|
|
|
let disconnected_listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let disconnected_exec_server_url = format!("ws://{}", disconnected_listener.local_addr()?);
|
|
let (disconnected_tx, disconnected_rx) = oneshot::channel();
|
|
let disconnected_exec_server = tokio::spawn(async move {
|
|
let (stream, _) = disconnected_listener.accept().await?;
|
|
let websocket = accept_async(stream).await?;
|
|
drop(websocket);
|
|
disconnected_tx
|
|
.send(())
|
|
.map_err(|()| anyhow::anyhow!("status test stopped before disconnect"))?;
|
|
Ok::<_, anyhow::Error>(())
|
|
});
|
|
|
|
let mut app_server = TestAppServer::builder().build().await?;
|
|
timeout(RPC_TIMEOUT, app_server.initialize()).await??;
|
|
let auto_environment_id = app_server.auto_env()?.selection().environment_id.clone();
|
|
|
|
add_environment(&mut app_server, "ready", &ready_exec_server_url).await?;
|
|
add_environment(&mut app_server, "pending", &pending_exec_server_url).await?;
|
|
add_environment(
|
|
&mut app_server,
|
|
"disconnected",
|
|
&disconnected_exec_server_url,
|
|
)
|
|
.await?;
|
|
timeout(RPC_TIMEOUT, ready_connected_rx).await??;
|
|
timeout(RPC_TIMEOUT, pending_connected_rx).await??;
|
|
timeout(RPC_TIMEOUT, disconnected_rx).await??;
|
|
|
|
assert_eq!(
|
|
wait_for_status(
|
|
&mut app_server,
|
|
&auto_environment_id,
|
|
EnvironmentStatusKind::Ready,
|
|
)
|
|
.await?,
|
|
EnvironmentStatusResponse {
|
|
status: EnvironmentStatusKind::Ready,
|
|
error: None,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
wait_for_status(&mut app_server, "ready", EnvironmentStatusKind::Ready).await?,
|
|
EnvironmentStatusResponse {
|
|
status: EnvironmentStatusKind::Ready,
|
|
error: None,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
read_environment_status(&mut app_server, "pending").await?,
|
|
EnvironmentStatusResponse {
|
|
status: EnvironmentStatusKind::Pending,
|
|
error: None,
|
|
}
|
|
);
|
|
let disconnected = wait_for_status(
|
|
&mut app_server,
|
|
"disconnected",
|
|
EnvironmentStatusKind::Disconnected,
|
|
)
|
|
.await?;
|
|
let disconnected_error = disconnected.error.clone();
|
|
assert!(disconnected_error.is_some());
|
|
assert_eq!(
|
|
disconnected,
|
|
EnvironmentStatusResponse {
|
|
status: EnvironmentStatusKind::Disconnected,
|
|
error: disconnected_error,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
read_environment_status(&mut app_server, "missing").await?,
|
|
EnvironmentStatusResponse {
|
|
status: EnvironmentStatusKind::Unknown,
|
|
error: Some("unknown environment id `missing`".to_string()),
|
|
}
|
|
);
|
|
|
|
let _ = ready_release_tx.send(());
|
|
let _ = pending_release_tx.send(());
|
|
timeout(RPC_TIMEOUT, ready_exec_server).await???;
|
|
timeout(RPC_TIMEOUT, pending_exec_server).await???;
|
|
timeout(RPC_TIMEOUT, disconnected_exec_server).await???;
|
|
Ok(())
|
|
}
|
|
|
|
async fn add_environment(
|
|
app_server: &mut TestAppServer,
|
|
environment_id: &str,
|
|
exec_server_url: &str,
|
|
) -> Result<()> {
|
|
let params = EnvironmentAddParams {
|
|
environment_id: environment_id.to_string(),
|
|
exec_server_url: exec_server_url.to_string(),
|
|
connect_timeout_ms: None,
|
|
};
|
|
let add_request_id = app_server
|
|
.send_raw_request("environment/add", Some(serde_json::to_value(params)?))
|
|
.await?;
|
|
let add_response: JSONRPCResponse = timeout(
|
|
RPC_TIMEOUT,
|
|
app_server.read_stream_until_response_message(RequestId::Integer(add_request_id)),
|
|
)
|
|
.await??;
|
|
let _: EnvironmentAddResponse = to_response(add_response)?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn read_environment_status(
|
|
app_server: &mut TestAppServer,
|
|
environment_id: &str,
|
|
) -> Result<EnvironmentStatusResponse> {
|
|
let params = EnvironmentStatusParams {
|
|
environment_id: environment_id.to_string(),
|
|
};
|
|
let request_id = app_server
|
|
.send_raw_request("environment/status", Some(serde_json::to_value(params)?))
|
|
.await?;
|
|
let response: JSONRPCResponse = timeout(
|
|
RPC_TIMEOUT,
|
|
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
to_response(response)
|
|
}
|
|
|
|
async fn wait_for_status(
|
|
app_server: &mut TestAppServer,
|
|
environment_id: &str,
|
|
expected: EnvironmentStatusKind,
|
|
) -> Result<EnvironmentStatusResponse> {
|
|
timeout(RPC_TIMEOUT, async {
|
|
loop {
|
|
let response = read_environment_status(app_server, environment_id).await?;
|
|
if response.status == expected {
|
|
return Ok(response);
|
|
}
|
|
sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?
|
|
}
|