mirror of
https://github.com/openai/codex.git
synced 2026-04-25 23:24:55 +00:00
## Summary - stop reserving a localhost port in the websocket tests before spawning the server - let the app-server bind `127.0.0.1:0` itself and read back the actual bound websocket address from stderr - update the websocket test helpers and callers to use the discovered address ## Why this fixes the flake The previous harness reserved a port in the test process, dropped it, and then asked the server process to bind that same address. On busy runners there is a race between releasing the reservation and the child process rebinding it, which can produce sporadic startup failures. Binding to port `0` inside the server removes that race entirely, and waiting for the server to report the real bound address makes the tests connect only after the listener is actually ready.
169 lines
6.5 KiB
Rust
169 lines
6.5 KiB
Rust
use super::connection_handling_websocket::DEFAULT_READ_TIMEOUT;
|
|
use super::connection_handling_websocket::WsClient;
|
|
use super::connection_handling_websocket::assert_no_message;
|
|
use super::connection_handling_websocket::connect_websocket;
|
|
use super::connection_handling_websocket::create_config_toml;
|
|
use super::connection_handling_websocket::read_notification_for_method;
|
|
use super::connection_handling_websocket::read_response_and_notification_for_method;
|
|
use super::connection_handling_websocket::read_response_for_id;
|
|
use super::connection_handling_websocket::send_initialize_request;
|
|
use super::connection_handling_websocket::send_request;
|
|
use super::connection_handling_websocket::spawn_websocket_server;
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use app_test_support::create_fake_rollout_with_text_elements;
|
|
use app_test_support::create_mock_responses_server_repeating_assistant;
|
|
use app_test_support::to_response;
|
|
use codex_app_server_protocol::JSONRPCNotification;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::ThreadNameUpdatedNotification;
|
|
use codex_app_server_protocol::ThreadResumeParams;
|
|
use codex_app_server_protocol::ThreadResumeResponse;
|
|
use codex_app_server_protocol::ThreadSetNameParams;
|
|
use codex_app_server_protocol::ThreadSetNameResponse;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
use tokio::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
#[tokio::test]
|
|
async fn thread_name_updated_broadcasts_for_loaded_threads() -> Result<()> {
|
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
|
let conversation_id = create_rollout(codex_home.path(), "2025-01-05T12-00-00")?;
|
|
|
|
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
|
|
|
let result = async {
|
|
let mut ws1 = connect_websocket(bind_addr).await?;
|
|
let mut ws2 = connect_websocket(bind_addr).await?;
|
|
initialize_both_clients(&mut ws1, &mut ws2).await?;
|
|
|
|
send_request(
|
|
&mut ws1,
|
|
"thread/resume",
|
|
10,
|
|
Some(serde_json::to_value(ThreadResumeParams {
|
|
thread_id: conversation_id.clone(),
|
|
..Default::default()
|
|
})?),
|
|
)
|
|
.await?;
|
|
let resume_resp: JSONRPCResponse = read_response_for_id(&mut ws1, 10).await?;
|
|
let resume: ThreadResumeResponse = to_response::<ThreadResumeResponse>(resume_resp)?;
|
|
assert_eq!(resume.thread.id, conversation_id);
|
|
|
|
let renamed = "Loaded rename";
|
|
send_request(
|
|
&mut ws1,
|
|
"thread/name/set",
|
|
11,
|
|
Some(serde_json::to_value(ThreadSetNameParams {
|
|
thread_id: conversation_id.clone(),
|
|
name: renamed.to_string(),
|
|
})?),
|
|
)
|
|
.await?;
|
|
let (rename_resp, ws1_notification) =
|
|
read_response_and_notification_for_method(&mut ws1, 11, "thread/name/updated").await?;
|
|
let _: ThreadSetNameResponse = to_response::<ThreadSetNameResponse>(rename_resp)?;
|
|
assert_thread_name_updated(ws1_notification, &conversation_id, renamed)?;
|
|
|
|
let ws2_notification =
|
|
read_notification_for_method(&mut ws2, "thread/name/updated").await?;
|
|
assert_thread_name_updated(ws2_notification, &conversation_id, renamed)?;
|
|
|
|
assert_no_message(&mut ws1, Duration::from_millis(250)).await?;
|
|
assert_no_message(&mut ws2, Duration::from_millis(250)).await?;
|
|
Ok(())
|
|
}
|
|
.await;
|
|
|
|
process
|
|
.kill()
|
|
.await
|
|
.context("failed to stop websocket app-server process")?;
|
|
result
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn thread_name_updated_broadcasts_for_not_loaded_threads() -> Result<()> {
|
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
|
let conversation_id = create_rollout(codex_home.path(), "2025-01-05T12-05-00")?;
|
|
|
|
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
|
|
|
let result = async {
|
|
let mut ws1 = connect_websocket(bind_addr).await?;
|
|
let mut ws2 = connect_websocket(bind_addr).await?;
|
|
initialize_both_clients(&mut ws1, &mut ws2).await?;
|
|
|
|
let renamed = "Stored rename";
|
|
send_request(
|
|
&mut ws1,
|
|
"thread/name/set",
|
|
20,
|
|
Some(serde_json::to_value(ThreadSetNameParams {
|
|
thread_id: conversation_id.clone(),
|
|
name: renamed.to_string(),
|
|
})?),
|
|
)
|
|
.await?;
|
|
let (rename_resp, ws1_notification) =
|
|
read_response_and_notification_for_method(&mut ws1, 20, "thread/name/updated").await?;
|
|
let _: ThreadSetNameResponse = to_response::<ThreadSetNameResponse>(rename_resp)?;
|
|
assert_thread_name_updated(ws1_notification, &conversation_id, renamed)?;
|
|
|
|
let ws2_notification =
|
|
read_notification_for_method(&mut ws2, "thread/name/updated").await?;
|
|
assert_thread_name_updated(ws2_notification, &conversation_id, renamed)?;
|
|
|
|
assert_no_message(&mut ws1, Duration::from_millis(250)).await?;
|
|
assert_no_message(&mut ws2, Duration::from_millis(250)).await?;
|
|
Ok(())
|
|
}
|
|
.await;
|
|
|
|
process
|
|
.kill()
|
|
.await
|
|
.context("failed to stop websocket app-server process")?;
|
|
result
|
|
}
|
|
|
|
async fn initialize_both_clients(ws1: &mut WsClient, ws2: &mut WsClient) -> Result<()> {
|
|
send_initialize_request(ws1, 1, "ws_client_one").await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(ws1, 1)).await??;
|
|
|
|
send_initialize_request(ws2, 2, "ws_client_two").await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(ws2, 2)).await??;
|
|
Ok(())
|
|
}
|
|
|
|
fn create_rollout(codex_home: &std::path::Path, filename_ts: &str) -> Result<String> {
|
|
create_fake_rollout_with_text_elements(
|
|
codex_home,
|
|
filename_ts,
|
|
"2025-01-05T12:00:00Z",
|
|
"Saved user message",
|
|
Vec::new(),
|
|
Some("mock_provider"),
|
|
None,
|
|
)
|
|
}
|
|
|
|
fn assert_thread_name_updated(
|
|
notification: JSONRPCNotification,
|
|
thread_id: &str,
|
|
thread_name: &str,
|
|
) -> Result<()> {
|
|
let notification: ThreadNameUpdatedNotification =
|
|
serde_json::from_value(notification.params.context("thread/name/updated params")?)?;
|
|
assert_eq!(notification.thread_id, thread_id);
|
|
assert_eq!(notification.thread_name.as_deref(), Some(thread_name));
|
|
Ok(())
|
|
}
|