Add app-server tests for sticky environments

Cover sticky thread environment selections and turn-level overrides through the app-server v2 thread/start and turn/start JSON-RPC flow. The matrix mirrors the manual smoke cases for omitted, empty, local, remote, and local plus remote selections.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-04-20 14:18:19 -07:00
parent e1d635f7a4
commit 7ce7f245c7

View File

@@ -5,6 +5,7 @@ use app_test_support::create_apply_patch_sse_response;
use app_test_support::create_exec_command_sse_response;
use app_test_support::create_fake_rollout;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::create_mock_responses_server_sequence_unchecked;
use app_test_support::create_shell_command_sse_response;
@@ -42,6 +43,7 @@ use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnEnvironmentParams;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStartedNotification;
@@ -1840,6 +1842,177 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn turn_start_resolves_sticky_thread_environments_and_turn_overrides() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let server = create_mock_responses_server_repeating_assistant("done").await;
create_config_toml(&codex_home, &server.uri(), "never", &BTreeMap::default())?;
let mut mcp = McpProcess::new_with_env(
&codex_home,
&[("CODEX_EXEC_SERVER_URL", Some("http://127.0.0.1:1"))],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
for case in [
EnvironmentSelectionCase {
name: "sticky_unset_turn_unset",
sticky: None,
turn: None,
},
EnvironmentSelectionCase {
name: "sticky_empty_turn_unset",
sticky: Some(&[]),
turn: None,
},
EnvironmentSelectionCase {
name: "sticky_local_turn_unset",
sticky: Some(&["local"]),
turn: None,
},
EnvironmentSelectionCase {
name: "sticky_remote_turn_unset",
sticky: Some(&["remote"]),
turn: None,
},
EnvironmentSelectionCase {
name: "sticky_local_remote_turn_unset",
sticky: Some(&["local", "remote"]),
turn: None,
},
EnvironmentSelectionCase {
name: "sticky_local_turn_empty",
sticky: Some(&["local"]),
turn: Some(&[]),
},
EnvironmentSelectionCase {
name: "sticky_empty_turn_local",
sticky: Some(&[]),
turn: Some(&["local"]),
},
EnvironmentSelectionCase {
name: "sticky_local_turn_remote",
sticky: Some(&["local"]),
turn: Some(&["remote"]),
},
EnvironmentSelectionCase {
name: "sticky_remote_turn_local",
sticky: Some(&["remote"]),
turn: Some(&["local"]),
},
EnvironmentSelectionCase {
name: "sticky_unset_turn_local_remote",
sticky: None,
turn: Some(&["local", "remote"]),
},
] {
run_environment_selection_case(&mut mcp, &workspace, case).await?;
}
Ok(())
}
struct EnvironmentSelectionCase {
name: &'static str,
sticky: Option<&'static [&'static str]>,
turn: Option<&'static [&'static str]>,
}
async fn run_environment_selection_case(
mcp: &mut McpProcess,
workspace: &Path,
case: EnvironmentSelectionCase,
) -> Result<()> {
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
cwd: Some(workspace.to_string_lossy().into_owned()),
environments: environment_params(case.sticky, workspace)?,
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: format!("run {}", case.name),
text_elements: Vec::new(),
}],
environments: environment_params(case.turn, workspace)?,
cwd: Some(workspace.to_path_buf()),
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
let started_notification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/started"),
)
.await??;
let started: TurnStartedNotification =
serde_json::from_value(started_notification.params.expect("turn/started params"))?;
assert_eq!(started.turn.id, turn.id, "{}", case.name);
let completed_notification = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let completed: TurnCompletedNotification = serde_json::from_value(
completed_notification
.params
.expect("turn/completed params"),
)?;
assert_eq!(completed.turn.id, turn.id, "{}", case.name);
assert_eq!(
completed.turn.status,
TurnStatus::Completed,
"{}",
case.name
);
mcp.clear_message_buffer();
Ok(())
}
fn environment_params(
ids: Option<&[&str]>,
cwd: &Path,
) -> Result<Option<Vec<TurnEnvironmentParams>>> {
ids.map(|ids| {
ids.iter()
.map(|id| {
Ok(TurnEnvironmentParams {
environment_id: (*id).to_string(),
cwd: cwd.to_path_buf().try_into()?,
})
})
.collect()
})
.transpose()
}
#[tokio::test]
async fn turn_start_file_change_approval_v2() -> Result<()> {
skip_if_no_network!(Ok(()));