Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions codex-rs/app-server-client/src/cloud_backend/connection_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,85 @@ use pretty_assertions::assert_eq;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::body_bytes;
use wiremock::matchers::method;
use wiremock::matchers::path;

#[tokio::test]
async fn rejected_rename_preserves_loaded_cloud_thread_name() -> Result<()> {
let server = MockServer::start().await;
let home = tempfile::tempdir()?;
let config = crate::legacy_core::config::ConfigBuilder::default()
.codex_home(home.path().to_owned())
.fallback_cwd(Some(home.path().to_owned()))
.without_project_context()
.build()
.await?;
let mut requests = requests::Requests {
client: std::sync::Arc::new(cloud::Client::new(
&config.http_client_factory(),
&server.uri(),
cloud::Credentials::new("synthetic-bearer", "synthetic-account")?,
)?),
account: protocol::GetAccountResponse {
user_id: None,
account: None,
requires_openai_auth: true,
workspace_routing: None,
},
loaded: Some(Box::new(serde_json::from_value(serde_json::json!({
"thread": {
"id": "t", "sessionId": "t", "preview": "", "ephemeral": false,
"modelProvider": "openai", "createdAt": 0, "updatedAt": 0,
"status": {"type": "idle"}, "cwd": home.path(), "cliVersion": "",
"source": "cli", "turns": [], "name": "Old",
},
"model": "test", "modelProvider": "openai", "cwd": home.path(),
"approvalPolicy": "never", "approvalsReviewer": "user",
"sandbox": {"type": "readOnly"},
}))?)),
warned_partial_history: None,
};
Mock::given(method("POST"))
.and(path(
"/oaiproto.codex_app_server_backend.ThreadService/SetThreadName",
))
.and(body_bytes(b"\x00\x00\x00\x00\x08\x0a\x01t\x12\x03New"))
.respond_with(ResponseTemplate::new(503))
.expect(1)
.mount(&server)
.await;

let (output, mut notifications) = mpsc::unbounded_channel();
let mut replies = Replies::default();
let read = protocol::ThreadReadParams {
thread_id: "t".into(),
include_turns: false,
};
let rejected = handle_request(
protocol::ClientRequest::ThreadSetName {
request_id: protocol::RequestId::Integer(1),
params: protocol::ThreadSetNameParams {
thread_id: "t".into(),
name: " New ".into(),
},
},
cloud::SubmissionMode::StartIfIdle,
/*project*/ None,
&mut requests,
&mut replies,
&output,
)
.await;
assert!(rejected.unwrap_err().is::<cloud::MutationError>());
assert_eq!(
requests.read_thread(&read, /*status*/ None)?["thread"]["name"],
"Old"
);
assert!(notifications.try_recv().is_err());
Ok(())
}

#[tokio::test]
async fn failed_archive_loaded_list_tracks_attachment_not_cached_metadata() -> Result<()> {
let server = MockServer::start().await;
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/app-server-client/src/cloud_backend/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ impl Requests {
}
serde_json::to_value(page)?
}
Request::ThreadSetName { params, .. } => {
let name = params.name.trim();
self.client.set_thread_name(&params.thread_id, name).await?;
if let Some(loaded) = &mut self.loaded
&& loaded.thread.id == params.thread_id
{
loaded.thread.name = Some(name.to_owned());
}
send_notification(
output,
protocol::ServerNotification::ThreadNameUpdated(
protocol::ThreadNameUpdatedNotification {
thread_id: params.thread_id,
thread_name: Some(name.to_owned()),
},
),
);
serde_json::to_value(protocol::ThreadSetNameResponse {})?
}
Request::ThreadArchive { params, .. } => {
self.client.archive_thread(&params.thread_id).await?;
return Ok(Response::Archived(params.thread_id));
Expand Down
27 changes: 27 additions & 0 deletions codex-rs/cloud-client/src/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,33 @@ async fn archive_addresses_the_requested_thread_and_requires_acknowledgment() {
assert!(fixture.requests.try_recv().is_err());
}

#[tokio::test]
async fn rename_sends_the_thread_and_title_and_requires_acknowledgment() {
let mut fixture = fixture_responses(vec![
response(vec![frame(&[])], tonic::Status::ok("")),
response(
vec![frame(&[])],
tonic::Status::permission_denied("synthetic denial"),
),
])
.await;

fixture.client.set_thread_name("t", "New").await.unwrap();
assert_request(&mut fixture, "SetThreadName", b"\x0a\x01t\x12\x03New").await;

let error = fixture
.client
.set_thread_name("u", "Other")
.await
.unwrap_err();
let MutationError::OutcomeUnknown(Error::Rpc(status)) = error else {
panic!("expected a remote rename failure");
};
assert_eq!(status.code, tonic::Code::PermissionDenied as i32);
assert_request(&mut fixture, "SetThreadName", b"\x0a\x01u\x12\x05Other").await;
assert!(fixture.requests.try_recv().is_err());
}

#[tokio::test]
async fn activation_preserves_native_settings_and_thread_metadata() {
// Native Thread fields encoded independently of the client's wire types.
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/cloud-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,25 @@ impl Client {
Ok(())
}

/// Saves a conversation name without opening an attachment.
pub async fn set_thread_name(&self, thread_id: &str, name: &str) -> Result<(), MutationError> {
if thread_id.trim().is_empty() || name.trim().is_empty() {
return Err(MutationError::InvalidRequest);
}
let payload = self
.mutate(
wire::SetThreadNameRequest {
thread_id: thread_id.to_owned(),
name: name.to_owned(),
},
"/oaiproto.codex_app_server_backend.ThreadService/SetThreadName",
)
.await?;
wire::EmptyResponse::decode(payload)
.map_err(|_| MutationError::OutcomeUnknown(Error::InvalidResponse))?;
Ok(())
}

/// Archives a conversation using the current account, without opening an attachment.
pub async fn archive_thread(&self, thread_id: &str) -> Result<(), MutationError> {
if thread_id.trim().is_empty() {
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/cloud-client/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ pub(crate) struct ThreadIdRequest {
pub thread_id: String,
}

#[derive(Clone, prost::Message)]
pub(crate) struct SetThreadNameRequest {
#[prost(string, tag = "1")]
pub thread_id: String,
#[prost(string, tag = "2")]
pub name: String,
}

#[derive(Clone, prost::Message)]
pub(crate) struct UnarchiveResponse {
#[prost(bytes = "bytes", optional, tag = "1")]
Expand Down
Loading