mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Support environment updates in app-server settings APIs (#60655)
## What changed Add experimental `environments` fields to `turn/settings/update` and `thread/settings/update`. Turn updates apply to later steps of the running turn; thread updates apply to future turns. Omission or `null` preserves the selection, while `[]` clears it. Providing `cwd` in a thread update without an environment list selects the defaults for that directory. Include the ordered environment selection in `thread/settings/updated`. Wait for Core to accept thread settings updates before returning success, and return rejections as request errors. ## Testing Add integration coverage for attaching, replacing, preserving, and clearing environments; rejected updates; and the distinction between later steps and future turns. GitOrigin-RevId: bfebeda57c676be94427aeb0c567c0b94fd9c387
This commit is contained in:
+26
@@ -99,6 +99,9 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -300,6 +303,29 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadEnvironment": {
|
||||
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"runtimeWorkspaceRoots": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"environmentId",
|
||||
"runtimeWorkspaceRoots"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSettings": {
|
||||
"properties": {
|
||||
"activePermissionProfile": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1113,6 +1113,7 @@ client_request_definitions! {
|
||||
#[experimental("turn/settings/update")]
|
||||
TurnSettingsUpdate => "turn/settings/update" {
|
||||
params: v2::TurnSettingsUpdateParams,
|
||||
inspect_params: true,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::TurnSettingsUpdateResponse,
|
||||
},
|
||||
@@ -4623,6 +4624,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
thread_settings: v2::ThreadSettings {
|
||||
disabled_plugin_ids: Vec::new(),
|
||||
environments: Some(Vec::new()),
|
||||
cwd: absolute_path("/tmp/repo"),
|
||||
approval_policy: v2::AskForApproval::Never,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::AskForApproval;
|
||||
use super::SandboxMode;
|
||||
use super::SandboxPolicy;
|
||||
use super::Thread;
|
||||
use super::ThreadEnvironment;
|
||||
use super::ThreadHistoryMode;
|
||||
use super::ThreadItem;
|
||||
use super::ThreadRealtimeItem;
|
||||
@@ -242,6 +243,12 @@ pub struct ThreadSettingsUpdateParams {
|
||||
/// Override the working directory for subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Replace the environments for future turns without changing the running turn.
|
||||
/// Omitted/null preserves the selection unless `cwd` is also provided, which selects
|
||||
/// the default environments for that directory. [] clears the selection.
|
||||
#[experimental("thread/settings/update.environments")]
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
/// Override the approval policy for subsequent turns.
|
||||
#[experimental(nested)]
|
||||
#[ts(optional = nullable)]
|
||||
@@ -306,6 +313,11 @@ pub struct ThreadSettings {
|
||||
#[serde(default)]
|
||||
pub disabled_plugin_ids: Vec<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Environments configured for future turns, in priority order, primary first.
|
||||
/// `null` means the server does not expose the selection; [] means none are selected.
|
||||
#[experimental("thread/settings.environments")]
|
||||
#[serde(default)]
|
||||
pub environments: Option<Vec<ThreadEnvironment>>,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
|
||||
@@ -43,7 +43,9 @@ pub enum TurnStatus {
|
||||
/// Unsupported fields are rejected rather than silently ignored.
|
||||
/// Any live task kind may accept publication. Child sessions and consumers of
|
||||
/// frozen initial settings are unchanged.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct TurnSettingsUpdateParams {
|
||||
@@ -53,6 +55,11 @@ pub struct TurnSettingsUpdateParams {
|
||||
/// Already captured steps and pending approvals retain their original reviewer.
|
||||
#[ts(optional = nullable)]
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
/// Replace the environments for subsequent steps of this turn, not future turns.
|
||||
/// Omitted/null preserves the selection; [] clears it. An already-started step is unchanged.
|
||||
#[experimental("turn/settings/update.environments")]
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
/// Omission or `null` leaves the model unchanged.
|
||||
#[ts(optional = nullable)]
|
||||
pub model: Option<String>,
|
||||
|
||||
@@ -516,6 +516,12 @@ Read the selection from `threadSettings.disabledPluginIds` in
|
||||
across resume. Forks restore the selection from the history retained at the
|
||||
requested fork boundary.
|
||||
|
||||
# Environment settings
|
||||
|
||||
`turn/settings/update` can replace the environments used by later steps of the running turn; an already-started step and future turns are unchanged. `thread/settings/update` changes the selection for future turns without affecting the running turn. Both accept the same `environments` list as `turn/start`: omission or `null` keeps the selection, and `[]` clears it. A thread settings request that provides `cwd` without an explicit environment list selects the default environments for that directory. Selected environments inherit the thread's configuration.
|
||||
|
||||
`thread/settings/update` returns success after Core accepts the update, including an unchanged selection. Rejections are returned as request errors. `thread/settings/updated` reports effective changes, including the ordered `environments` selection, and is not a per-request acknowledgment. Updating both scopes requires two separate requests; they are not atomic.
|
||||
|
||||
# Deprecated thread personality setting
|
||||
|
||||
`thread/start`, `thread/resume`, `thread/settings/update`, and `turn/start` still
|
||||
|
||||
@@ -178,6 +178,13 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
||||
ThreadSettings {
|
||||
disabled_plugin_ids: config_snapshot.disabled_plugin_ids.clone(),
|
||||
cwd: config_snapshot.cwd().clone(),
|
||||
environments: Some(
|
||||
config_snapshot
|
||||
.environment_selections()
|
||||
.iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
),
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
|
||||
sandbox_policy: config_snapshot.sandbox_policy().into(),
|
||||
|
||||
@@ -257,6 +257,8 @@ impl TurnRequestProcessor {
|
||||
let (_, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
let environments =
|
||||
resolve_turn_environment_requests(self.thread_manager.as_ref(), params.environments)?;
|
||||
let (reply, outcome) = oneshot::channel();
|
||||
self.submit_core_op(
|
||||
request_id,
|
||||
@@ -267,7 +269,7 @@ impl TurnRequestProcessor {
|
||||
approvals_reviewer: params
|
||||
.approvals_reviewer
|
||||
.map(codex_app_server_protocol::ApprovalsReviewer::to_core),
|
||||
environments: None,
|
||||
environments,
|
||||
model: params.model,
|
||||
// Match thread/settings/update: public null does not clear effort.
|
||||
effort: params.effort.map(Some),
|
||||
@@ -861,9 +863,6 @@ impl TurnRequestProcessor {
|
||||
let collaboration_mode =
|
||||
collaboration_mode.map(|mode| self.normalize_collaboration_mode(mode));
|
||||
let has_environment_override = environment_requests.is_some();
|
||||
// `thread/settings/update` only acknowledges that the update was queued.
|
||||
// Clients that send dependent partial updates should wait for
|
||||
// `thread/settings/updated` or combine the fields in one request.
|
||||
let snapshot = if permissions.is_some() {
|
||||
Some(thread.config_snapshot().await)
|
||||
} else {
|
||||
@@ -982,12 +981,14 @@ impl TurnRequestProcessor {
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
let cwd = resolve_request_cwd(params.cwd)?;
|
||||
let environments =
|
||||
resolve_turn_environment_requests(self.thread_manager.as_ref(), params.environments)?;
|
||||
let environment_override = self
|
||||
.build_environment_override(
|
||||
thread.as_ref(),
|
||||
cwd,
|
||||
/*workspace_roots*/ None,
|
||||
/*environment_requests*/ None,
|
||||
environments,
|
||||
)
|
||||
.await;
|
||||
let thread_settings = self
|
||||
@@ -1012,16 +1013,24 @@ impl TurnRequestProcessor {
|
||||
.await?;
|
||||
|
||||
if thread_settings != codex_protocol::protocol::ThreadSettingsOverrides::default() {
|
||||
let (reply, outcome) = oneshot::channel();
|
||||
self.submit_core_op(
|
||||
request_id,
|
||||
thread.as_ref(),
|
||||
Op::ThreadSettings {
|
||||
thread_settings,
|
||||
reply: None,
|
||||
reply: Some(reply),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to update thread settings: {err}")))?;
|
||||
outcome
|
||||
.await
|
||||
.map_err(|_| internal_error("thread settings operation ended before replying"))?
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
_ => internal_error(format!("failed to update thread settings: {err}")),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(ThreadSettingsUpdateResponse {})
|
||||
|
||||
@@ -333,6 +333,7 @@ mod tests {
|
||||
fn thread_settings(model: &str) -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
disabled_plugin_ids: Vec::new(),
|
||||
environments: Some(Vec::new()),
|
||||
cwd: AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute path"),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_app_server_protocol::ThreadEnvironment;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
@@ -49,6 +50,127 @@ use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Selection-only changes are observable after acknowledgment, including attaching and clearing.
|
||||
#[tokio::test]
|
||||
async fn environments_attach_replace_preserve_and_clear_without_inference() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("environments.toml"),
|
||||
r#"
|
||||
[[environments]]
|
||||
id = "remote-a"
|
||||
url = "ws://127.0.0.1:1"
|
||||
[[environments]]
|
||||
id = "remote-b"
|
||||
url = "ws://127.0.0.1:2"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("requirements.toml"),
|
||||
"allowed_approval_policies = [\"never\"]\n",
|
||||
)?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
// This test owns environments.toml and starts with no selected executor.
|
||||
.without_auto_env()
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
cwd: Some(codex_home.path().to_string_lossy().into_owned()),
|
||||
environments: Some(Vec::new()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let started: ThreadStartResponse = mcp.read_response(request_id).await?;
|
||||
let environment_a = ThreadEnvironment {
|
||||
environment_id: "remote-a".to_string(),
|
||||
cwd: started.cwd.clone().into(),
|
||||
runtime_workspace_roots: vec![started.cwd.clone().into()],
|
||||
};
|
||||
let environment_b = ThreadEnvironment {
|
||||
environment_id: "remote-b".to_string(),
|
||||
..environment_a.clone()
|
||||
};
|
||||
let rejected_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/settings/update",
|
||||
Some(json!({
|
||||
"threadId": started.thread.id,
|
||||
"environments": [environment_a],
|
||||
"approvalPolicy": "on-request",
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let rejected = mcp
|
||||
.read_stream_until_error_message(RequestId::Integer(rejected_id))
|
||||
.await?;
|
||||
assert_eq!(rejected.error.code, -32600);
|
||||
assert!(rejected.error.message.contains("approval_policy"));
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: started.thread.id.clone(),
|
||||
include_turns: false,
|
||||
})
|
||||
.await?;
|
||||
let read: ThreadReadResponse = mcp.read_response(read_id).await?;
|
||||
assert_eq!(read.thread.environments, Some(Vec::new()));
|
||||
|
||||
for (mut params, expected, changed) in [
|
||||
(
|
||||
json!({"environments": [environment_a]}),
|
||||
vec![environment_a],
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!({"environments": [environment_b]}),
|
||||
vec![environment_b.clone()],
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!({"environments": [environment_b]}),
|
||||
vec![environment_b.clone()],
|
||||
false,
|
||||
),
|
||||
(json!({}), vec![environment_b.clone()], false),
|
||||
(json!({"environments": null}), vec![environment_b], false),
|
||||
(json!({"environments": []}), vec![], true),
|
||||
] {
|
||||
params["threadId"] = json!(started.thread.id);
|
||||
let request_id = mcp
|
||||
.send_raw_request("thread/settings/update", Some(params))
|
||||
.await?;
|
||||
let _: ThreadSettingsUpdateResponse = mcp.read_response(request_id).await?;
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: started.thread.id.clone(),
|
||||
include_turns: false,
|
||||
})
|
||||
.await?;
|
||||
let read: ThreadReadResponse = mcp.read_response(read_id).await?;
|
||||
assert_eq!(read.thread.environments, Some(expected.clone()));
|
||||
if changed {
|
||||
let updated = read_thread_settings_updated(&mut mcp).await?;
|
||||
assert_eq!(
|
||||
(
|
||||
updated.thread_id,
|
||||
updated.thread_settings.cwd,
|
||||
updated.thread_settings.environments,
|
||||
),
|
||||
(
|
||||
started.thread.id.clone(),
|
||||
started.cwd.clone(),
|
||||
Some(expected.clone()),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(received_response_bodies(&server).await?.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_plugin_ids_replace_preserve_and_clear_without_inference() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
|
||||
@@ -2,12 +2,15 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::MockResponsesConfig;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::create_request_user_input_sse_response;
|
||||
use codex_app_server_protocol::ApprovalsReviewer;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdateParams;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdateResponse;
|
||||
use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
|
||||
@@ -21,6 +24,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_features::Feature;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
@@ -48,6 +52,118 @@ enum UpdateScenario {
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// Environment updates take effect at their requested scope without enabling model switching.
|
||||
#[test_case("turn/settings/update", [false, true, false]; "later steps only")]
|
||||
#[test_case("thread/settings/update", [false, false, true]; "future turns only")]
|
||||
#[tokio::test]
|
||||
async fn environment_settings_updates_use_the_requested_scope(
|
||||
method: &str,
|
||||
expected_new_cwd: [bool; 3],
|
||||
) -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let requests = responses::mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
create_request_user_input_sse_response("pause")?,
|
||||
responses::sse_completed("after-update"),
|
||||
responses::sse_completed("next-turn"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let codex_home = TempDir::new()?;
|
||||
mock_config(codex_home.path(), &server.uri())?.write(codex_home.path())?;
|
||||
let mut app = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
|
||||
.await?;
|
||||
let original_cwd = app.auto_env()?.selection().cwd.clone();
|
||||
let new_cwd = original_cwd.join("settings-environment")?;
|
||||
app.auto_env()?
|
||||
.environment()
|
||||
.get_filesystem()
|
||||
.create_directory(
|
||||
&new_cwd,
|
||||
CreateDirectoryOptions {
|
||||
recursive: false,
|
||||
follow_symlinks: true,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
let mut environment = app.auto_env_params()?;
|
||||
environment.cwd = new_cwd.clone().into();
|
||||
let thread_request = app
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
|
||||
.await?;
|
||||
let ThreadStartResponse { thread, .. } = app.read_response(thread_request).await?;
|
||||
let turn_id = start_turn(&mut app, &thread.id).await?;
|
||||
let request = timeout(DEFAULT_TIMEOUT, app.read_stream_until_request_message()).await??;
|
||||
let ServerRequest::ToolRequestUserInput { request_id, .. } = request else {
|
||||
anyhow::bail!("expected paused turn, received {request:?}");
|
||||
};
|
||||
|
||||
let mut update = json!({"threadId": thread.id, "environments": [environment]});
|
||||
if method == "turn/settings/update" {
|
||||
update["turnId"] = json!(turn_id);
|
||||
}
|
||||
let mut invalid_update = update.clone();
|
||||
invalid_update["environments"][0]["environmentId"] = json!("missing");
|
||||
let invalid_id = app.send_request(method, Some(invalid_update)).await?;
|
||||
let rejected = app
|
||||
.read_stream_until_error_message(RequestId::Integer(invalid_id))
|
||||
.await?;
|
||||
assert_eq!(
|
||||
rejected.error,
|
||||
JSONRPCErrorError {
|
||||
code: -32600,
|
||||
message: "unknown turn environment id `missing`".to_string(),
|
||||
data: None,
|
||||
}
|
||||
);
|
||||
let update_id = app.send_request(method, Some(update)).await?;
|
||||
let result: Value = app.read_response(update_id).await?;
|
||||
if method == "turn/settings/update" {
|
||||
assert_eq!(result, json!({"status": "applied"}));
|
||||
} else {
|
||||
assert_eq!(result, json!({}));
|
||||
let updated: ThreadSettingsUpdatedNotification =
|
||||
app.read_notification("thread/settings/updated").await?;
|
||||
assert_eq!(
|
||||
serde_json::to_value(updated.thread_settings.environments)?,
|
||||
json!([{
|
||||
"environmentId": environment.environment_id,
|
||||
"cwd": environment.cwd,
|
||||
"runtimeWorkspaceRoots": [new_cwd.inferred_native_path_string()],
|
||||
}])
|
||||
);
|
||||
}
|
||||
app.send_response(
|
||||
request_id,
|
||||
json!({"answers": {"confirm_path": {"answers": ["Yes (Recommended)"]}}}),
|
||||
)
|
||||
.await?;
|
||||
let _: TurnCompletedNotification = app.read_notification("turn/completed").await?;
|
||||
start_turn(&mut app, &thread.id).await?;
|
||||
let _: TurnCompletedNotification = app.read_notification("turn/completed").await?;
|
||||
|
||||
let original_cwd = format!("<cwd>{}</cwd>", original_cwd.inferred_native_path_string());
|
||||
let new_cwd = format!("<cwd>{}</cwd>", new_cwd.inferred_native_path_string());
|
||||
let visible_cwds = requests
|
||||
.requests()
|
||||
.iter()
|
||||
.map(|request| {
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.rfind(|text| text.contains(&new_cwd) || text.contains(&original_cwd))
|
||||
.expect("model request should include a working directory")
|
||||
.contains(&new_cwd)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(visible_cwds, expected_new_cwd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(UpdateScenario::Future; "future-only A A B")]
|
||||
#[test_case(UpdateScenario::Current; "turn-only A B A")]
|
||||
#[test_case(UpdateScenario::Rejected; "rejected turn update changes neither owner")]
|
||||
@@ -311,6 +427,7 @@ async fn turn_settings_update_reports_unavailable_without_starting_or_changing_a
|
||||
let update = TurnSettingsUpdateParams {
|
||||
thread_id: thread.id.clone(),
|
||||
turn_id: "never-started".to_string(),
|
||||
environments: Some(Vec::new()),
|
||||
model: Some(MODEL_B.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -353,6 +470,14 @@ async fn turn_settings_update_reports_unavailable_without_starting_or_changing_a
|
||||
requests.single_request().body_json()["model"],
|
||||
Value::String(MODEL_A.to_string())
|
||||
);
|
||||
let read_id = app
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread.id,
|
||||
include_turns: false,
|
||||
})
|
||||
.await?;
|
||||
let read: ThreadReadResponse = app.read_response(read_id).await?;
|
||||
assert_eq!(read.thread.environments, thread.environments);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,7 @@ mod tests {
|
||||
fn test_thread_settings() -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
disabled_plugin_ids: Vec::new(),
|
||||
environments: None,
|
||||
cwd: test_path_buf("/tmp/thread-settings").abs(),
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::Never,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
|
||||
|
||||
@@ -12869,6 +12869,7 @@ async fn inactive_thread_settings_notification_updates_cached_collaboration_mode
|
||||
thread_id: inactive_thread_id.to_string(),
|
||||
thread_settings: ThreadSettings {
|
||||
disabled_plugin_ids: Vec::new(),
|
||||
environments: None,
|
||||
cwd: test_absolute_path("/tmp/thread-settings"),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::AutoReview,
|
||||
|
||||
@@ -16,6 +16,7 @@ fn thread_settings_for_test(
|
||||
thread_id: thread_id.to_string(),
|
||||
thread_settings: codex_app_server_protocol::ThreadSettings {
|
||||
disabled_plugin_ids: Vec::new(),
|
||||
environments: None,
|
||||
cwd: test_path_buf("/tmp/thread-settings").abs(),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::AutoReview,
|
||||
|
||||
Reference in New Issue
Block a user