mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Add executor-local config reads to the exec server (#37408)
## What changed - Add the `environmentConfig/read` RPC for selecting literal TOML paths from executor-local config and requirements layers. - Return layer precedence, cloud insertion points, source and base-directory metadata, and executor home and hostname context without normalizing path-bearing values. - Expose the operation through both remote clients and local `Environment` instances, with invalid selectors reported as invalid parameters. ## Testing - Cover projected remote config reads and rejection of empty selectors. GitOrigin-RevId: fee15bf833de6bcb2058d405ed5b1d5d928d7218
This commit is contained in:
Generated
+3
@@ -2994,6 +2994,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"clatter",
|
||||
"codex-api",
|
||||
"codex-config",
|
||||
"codex-exec-server-protocol",
|
||||
"codex-exec-server-test-support",
|
||||
"codex-file-system",
|
||||
@@ -3004,11 +3005,13 @@ dependencies = [
|
||||
"codex-sandboxing",
|
||||
"codex-test-binary-support",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-home-dir",
|
||||
"codex-utils-path-uri",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-rustls-provider",
|
||||
"codex-websocket-client",
|
||||
"ctor 0.6.3",
|
||||
"dirs",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
"libc",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const ENVIRONMENT_CONFIG_READ_METHOD: &str = "environmentConfig/read";
|
||||
|
||||
/// Selects executor-local config and requirements fields by literal TOML key path.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnvironmentConfigReadParams {
|
||||
pub cwd: PathUri,
|
||||
pub config_paths: Vec<Vec<String>>,
|
||||
pub requirements_paths: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Executor-local config and requirements layers selected for one environment.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnvironmentConfigReadResponse {
|
||||
/// Executor user home used to expand `~` in path-bearing values.
|
||||
pub user_home_dir: Option<PathUri>,
|
||||
/// Executor Codex home used as the base directory for cloud-provided layers.
|
||||
pub codex_home_dir: PathUri,
|
||||
/// Executor hostname used to select matching remote sandbox requirements.
|
||||
pub hostname: Option<String>,
|
||||
pub config: EnvironmentConfigLayerStack,
|
||||
pub requirements: EnvironmentConfigLayerStack,
|
||||
}
|
||||
|
||||
/// One ordered set of selected TOML layers.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnvironmentConfigLayerStack {
|
||||
/// Layers ordered from lowest to highest precedence.
|
||||
pub layers: Vec<EnvironmentConfigLayer>,
|
||||
/// Position at which cloud-provided layers should be inserted.
|
||||
pub cloud_insertion_index: usize,
|
||||
}
|
||||
|
||||
/// One selected executor-local TOML layer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnvironmentConfigLayer {
|
||||
/// Opaque provenance for diagnostics. Callers must not derive behavior from it.
|
||||
pub source: String,
|
||||
/// Directory used to interpret relative paths in this layer.
|
||||
pub base_dir: PathUri,
|
||||
/// Selected raw TOML. Path-bearing values have not been normalized.
|
||||
pub toml: String,
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
mod environment_config;
|
||||
mod network_policy;
|
||||
mod process_id;
|
||||
mod protocol;
|
||||
pub mod rpc;
|
||||
|
||||
pub use environment_config::*;
|
||||
pub use network_policy::*;
|
||||
pub use process_id::ProcessId;
|
||||
pub use protocol::*;
|
||||
|
||||
@@ -17,6 +17,7 @@ base64 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
clatter = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-exec-server-protocol = { workspace = true }
|
||||
codex-file-system = { workspace = true }
|
||||
@@ -25,10 +26,12 @@ codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
codex-websocket-client = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
http = { workspace = true }
|
||||
prost = "0.14.3"
|
||||
|
||||
@@ -45,6 +45,7 @@ use crate::process::ExecProcessEventReceiver;
|
||||
use crate::protocol::CAPABILITY_ROOTS_DISCOVER_METHOD;
|
||||
use crate::protocol::CapabilityRootsDiscoverParams;
|
||||
use crate::protocol::CapabilityRootsDiscoverResponse;
|
||||
use crate::protocol::ENVIRONMENT_CONFIG_READ_METHOD;
|
||||
use crate::protocol::ENVIRONMENT_INFO_METHOD;
|
||||
use crate::protocol::ENVIRONMENT_STATUS_METHOD;
|
||||
use crate::protocol::EXEC_CLOSED_METHOD;
|
||||
@@ -55,6 +56,8 @@ use crate::protocol::EXEC_READ_METHOD;
|
||||
use crate::protocol::EXEC_SIGNAL_METHOD;
|
||||
use crate::protocol::EXEC_TERMINATE_METHOD;
|
||||
use crate::protocol::EXEC_WRITE_METHOD;
|
||||
use crate::protocol::EnvironmentConfigReadParams;
|
||||
use crate::protocol::EnvironmentConfigReadResponse;
|
||||
use crate::protocol::EnvironmentInfo;
|
||||
use crate::protocol::EnvironmentStatus;
|
||||
use crate::protocol::ExecClosedNotification;
|
||||
@@ -736,6 +739,13 @@ impl ExecServerClient {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn read_environment_config(
|
||||
&self,
|
||||
params: EnvironmentConfigReadParams,
|
||||
) -> Result<EnvironmentConfigReadResponse, ExecServerError> {
|
||||
self.call(ENVIRONMENT_CONFIG_READ_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn environment_status(&self) -> Result<EnvironmentStatus, ExecServerError> {
|
||||
// Health checks only reuse an existing RPC connection and never initiate recovery.
|
||||
let rpc_client = self.rpc_client_without_recovery()?;
|
||||
|
||||
@@ -12,6 +12,8 @@ use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
|
||||
use crate::CapabilityRootsDiscoverParams;
|
||||
use crate::CapabilityRootsDiscoverResponse;
|
||||
use crate::EnvironmentConfigReadParams;
|
||||
use crate::EnvironmentConfigReadResponse;
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
@@ -24,6 +26,7 @@ use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT;
|
||||
use crate::client_api::ExecServerTransportParams;
|
||||
use crate::environment_bootstrap::PreparedEnvironmentManager;
|
||||
use crate::environment_bootstrap::PreparedEnvironmentSource;
|
||||
use crate::environment_config::read_environment_config;
|
||||
use crate::environment_provider::DefaultEnvironmentProvider;
|
||||
use crate::environment_provider::EnvironmentDefault;
|
||||
use crate::environment_provider::EnvironmentProvider;
|
||||
@@ -881,6 +884,19 @@ impl Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads selected executor-local configuration fields for this environment.
|
||||
pub async fn read_environment_config(
|
||||
&self,
|
||||
params: EnvironmentConfigReadParams,
|
||||
) -> Result<EnvironmentConfigReadResponse, ExecServerError> {
|
||||
match &self.remote_client {
|
||||
Some(client) => client.get().await?.read_environment_config(params).await,
|
||||
None => read_environment_config(self.filesystem.as_ref(), params)
|
||||
.await
|
||||
.map_err(|error| ExecServerError::Protocol(error.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovers plugin and skill manifests through the environment's high-level discovery API.
|
||||
pub async fn discover_capability_roots(
|
||||
&self,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::format_config_layer_source;
|
||||
use codex_config::host_name;
|
||||
use codex_config::loader::LocalTomlLayerStack;
|
||||
use codex_config::loader::load_local_config_layers;
|
||||
use codex_exec_server_protocol::EnvironmentConfigLayer;
|
||||
use codex_exec_server_protocol::EnvironmentConfigLayerStack;
|
||||
use codex_exec_server_protocol::EnvironmentConfigReadParams;
|
||||
use codex_exec_server_protocol::EnvironmentConfigReadResponse;
|
||||
use codex_file_system::ExecutorFileSystem;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ReadEnvironmentConfigError {
|
||||
#[error("{0}")]
|
||||
InvalidParams(String),
|
||||
#[error("{0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
pub(crate) async fn read_environment_config(
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
params: EnvironmentConfigReadParams,
|
||||
) -> Result<EnvironmentConfigReadResponse, ReadEnvironmentConfigError> {
|
||||
validate_paths(¶ms)?;
|
||||
let cwd = params
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|error| ReadEnvironmentConfigError::InvalidParams(error.to_string()))?;
|
||||
let codex_home = find_codex_home().map_err(|error| {
|
||||
ReadEnvironmentConfigError::Internal(format!("failed to find Codex home: {error}"))
|
||||
})?;
|
||||
let layers = load_local_config_layers(file_system, codex_home.as_path(), &cwd)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
ReadEnvironmentConfigError::Internal(format!(
|
||||
"failed to load executor-local config: {error}"
|
||||
))
|
||||
})?
|
||||
.project(¶ms.config_paths, ¶ms.requirements_paths);
|
||||
|
||||
Ok(EnvironmentConfigReadResponse {
|
||||
user_home_dir: dirs::home_dir()
|
||||
.and_then(|home_dir| PathUri::from_host_native_path(home_dir).ok()),
|
||||
codex_home_dir: PathUri::from_abs_path(&codex_home),
|
||||
hostname: host_name(),
|
||||
config: serialize_layer_stack(layers.config, |source| {
|
||||
format_config_layer_source(source, CONFIG_TOML_FILE)
|
||||
})?,
|
||||
requirements: serialize_layer_stack(layers.requirements, ToString::to_string)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_paths(params: &EnvironmentConfigReadParams) -> Result<(), ReadEnvironmentConfigError> {
|
||||
if params.config_paths.is_empty() && params.requirements_paths.is_empty() {
|
||||
return Err(ReadEnvironmentConfigError::InvalidParams(
|
||||
"at least one config or requirements path is required".to_string(),
|
||||
));
|
||||
}
|
||||
if params
|
||||
.config_paths
|
||||
.iter()
|
||||
.chain(¶ms.requirements_paths)
|
||||
.any(Vec::is_empty)
|
||||
{
|
||||
return Err(ReadEnvironmentConfigError::InvalidParams(
|
||||
"TOML paths must contain at least one key segment".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_layer_stack<S>(
|
||||
stack: LocalTomlLayerStack<S>,
|
||||
source_name: impl Fn(&S) -> String,
|
||||
) -> Result<EnvironmentConfigLayerStack, ReadEnvironmentConfigError> {
|
||||
let layers = stack
|
||||
.layers
|
||||
.into_iter()
|
||||
.map(|layer| {
|
||||
let toml = toml::to_string(&layer.toml).map_err(|error| {
|
||||
ReadEnvironmentConfigError::Internal(format!(
|
||||
"failed to serialize executor-local config: {error}"
|
||||
))
|
||||
})?;
|
||||
Ok(EnvironmentConfigLayer {
|
||||
source: source_name(&layer.source),
|
||||
base_dir: PathUri::from_abs_path(&layer.base_dir),
|
||||
toml,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, ReadEnvironmentConfigError>>()?;
|
||||
Ok(EnvironmentConfigLayerStack {
|
||||
layers,
|
||||
cloud_insertion_index: stack.cloud_insertion_index,
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod client_transport;
|
||||
mod connection;
|
||||
mod environment;
|
||||
mod environment_bootstrap;
|
||||
mod environment_config;
|
||||
mod environment_provider;
|
||||
mod environment_registry;
|
||||
mod environment_toml;
|
||||
@@ -123,6 +124,10 @@ pub use protocol::CapabilityTextFile;
|
||||
pub use protocol::DiscoveredPluginFiles;
|
||||
pub use protocol::DiscoveredSkillFiles;
|
||||
pub use protocol::EnvironmentCapabilities;
|
||||
pub use protocol::EnvironmentConfigLayer;
|
||||
pub use protocol::EnvironmentConfigLayerStack;
|
||||
pub use protocol::EnvironmentConfigReadParams;
|
||||
pub use protocol::EnvironmentConfigReadResponse;
|
||||
pub use protocol::EnvironmentInfo;
|
||||
pub use protocol::EnvironmentStatus;
|
||||
pub use protocol::EnvironmentStatusKind;
|
||||
|
||||
@@ -16,8 +16,12 @@ use crate::ExecServerRuntimePaths;
|
||||
use crate::client::http_client::PendingRouteAwareHttpBodyStream;
|
||||
use crate::client::http_client::RouteAwareHttpClient;
|
||||
use crate::client::http_client::RouteAwareHttpRequestRunner;
|
||||
use crate::environment_config::ReadEnvironmentConfigError;
|
||||
use crate::environment_config::read_environment_config;
|
||||
use crate::protocol::CapabilityRootsDiscoverParams;
|
||||
use crate::protocol::CapabilityRootsDiscoverResponse;
|
||||
use crate::protocol::EnvironmentConfigReadParams;
|
||||
use crate::protocol::EnvironmentConfigReadResponse;
|
||||
use crate::protocol::EnvironmentInfo;
|
||||
use crate::protocol::EnvironmentStatus;
|
||||
use crate::protocol::EnvironmentStatusKind;
|
||||
@@ -175,6 +179,19 @@ impl ExecServerHandler {
|
||||
Ok(EnvironmentInfo::local())
|
||||
}
|
||||
|
||||
pub(crate) async fn environment_config_read(
|
||||
&self,
|
||||
params: EnvironmentConfigReadParams,
|
||||
) -> Result<EnvironmentConfigReadResponse, JSONRPCErrorError> {
|
||||
self.require_initialized_for("environment config")?;
|
||||
read_environment_config(crate::LOCAL_FS.as_ref(), params)
|
||||
.await
|
||||
.map_err(|error| match error {
|
||||
ReadEnvironmentConfigError::InvalidParams(message) => invalid_params(message),
|
||||
ReadEnvironmentConfigError::Internal(message) => internal_error(message),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn environment_status(&self) -> Result<EnvironmentStatus, JSONRPCErrorError> {
|
||||
self.require_initialized_for("environment status")?;
|
||||
Ok(EnvironmentStatus {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::protocol::CAPABILITY_ROOTS_DISCOVER_METHOD;
|
||||
use crate::protocol::CapabilityRootsDiscoverParams;
|
||||
use crate::protocol::ENVIRONMENT_CONFIG_READ_METHOD;
|
||||
use crate::protocol::ENVIRONMENT_INFO_METHOD;
|
||||
use crate::protocol::ENVIRONMENT_STATUS_METHOD;
|
||||
use crate::protocol::EXEC_METHOD;
|
||||
@@ -9,6 +10,7 @@ use crate::protocol::EXEC_READ_METHOD;
|
||||
use crate::protocol::EXEC_SIGNAL_METHOD;
|
||||
use crate::protocol::EXEC_TERMINATE_METHOD;
|
||||
use crate::protocol::EXEC_WRITE_METHOD;
|
||||
use crate::protocol::EnvironmentConfigReadParams;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::FS_CANONICALIZE_METHOD;
|
||||
use crate::protocol::FS_CLOSE_METHOD;
|
||||
@@ -74,6 +76,12 @@ pub(crate) fn build_router() -> RpcRouter<ExecServerHandler> {
|
||||
ENVIRONMENT_INFO_METHOD,
|
||||
|handler: Arc<ExecServerHandler>, _params: ()| async move { handler.environment_info() },
|
||||
);
|
||||
router.request(
|
||||
ENVIRONMENT_CONFIG_READ_METHOD,
|
||||
|handler: Arc<ExecServerHandler>, params: EnvironmentConfigReadParams| async move {
|
||||
handler.environment_config_read(params).await
|
||||
},
|
||||
);
|
||||
router.request(
|
||||
ENVIRONMENT_STATUS_METHOD,
|
||||
|handler: Arc<ExecServerHandler>, _params: ()| async move { handler.environment_status() },
|
||||
|
||||
@@ -32,7 +32,7 @@ const CONNECT_RETRY_INTERVAL: Duration = Duration::from_millis(25);
|
||||
const EVENT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub(crate) struct ExecServerHarness {
|
||||
_codex_home: TempDir,
|
||||
codex_home: TempDir,
|
||||
_helper_paths: TestCodexHelperPaths,
|
||||
child: Child,
|
||||
websocket_url: String,
|
||||
@@ -104,7 +104,7 @@ where
|
||||
let websocket_url = read_listen_url_from_stdout(&mut child).await?;
|
||||
let (websocket, _) = connect_websocket_when_ready(&websocket_url).await?;
|
||||
Ok(ExecServerHarness {
|
||||
_codex_home: codex_home,
|
||||
codex_home,
|
||||
_helper_paths: helper_paths,
|
||||
child,
|
||||
websocket_url,
|
||||
@@ -114,6 +114,10 @@ where
|
||||
}
|
||||
|
||||
impl ExecServerHarness {
|
||||
pub(crate) fn codex_home(&self) -> &std::path::Path {
|
||||
self.codex_home.path()
|
||||
}
|
||||
|
||||
pub(crate) fn websocket_url(&self) -> &str {
|
||||
&self.websocket_url
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
mod common;
|
||||
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::format_config_layer_source;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::EnvironmentConfigLayer;
|
||||
use codex_exec_server::EnvironmentConfigLayerStack;
|
||||
use codex_exec_server::EnvironmentConfigReadParams;
|
||||
use codex_exec_server::EnvironmentConfigReadResponse;
|
||||
use codex_exec_server::ExecServerError;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use common::exec_server::exec_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_environment_reads_projected_executor_config() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
let codex_home =
|
||||
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(server.codex_home())?)?;
|
||||
let config_file = codex_home.join(CONFIG_TOML_FILE);
|
||||
let project = codex_home.join("project");
|
||||
let dot_codex = project.join(".codex");
|
||||
tokio::fs::create_dir_all(dot_codex.as_path()).await?;
|
||||
tokio::fs::write(project.join(".project-root").as_path(), "").await?;
|
||||
let project_key = toml::Value::String(project_trust_key(project.as_path())).to_string();
|
||||
tokio::fs::write(
|
||||
&config_file,
|
||||
format!(
|
||||
"project_root_markers = [\".project-root\"]\n[projects.{project_key}]\ntrust_level = \"trusted\""
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::write(
|
||||
dot_codex.join(CONFIG_TOML_FILE).as_path(),
|
||||
r#"
|
||||
[future_environment]
|
||||
relative_path = "./executor-relative"
|
||||
unselected = "do not return"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
|
||||
let response = environment
|
||||
.read_environment_config(EnvironmentConfigReadParams {
|
||||
cwd: PathUri::from_abs_path(&project),
|
||||
config_paths: vec![vec![
|
||||
"future_environment".to_string(),
|
||||
"relative_path".to_string(),
|
||||
]],
|
||||
requirements_paths: Vec::new(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let projected_toml = toml::toml! {
|
||||
[future_environment]
|
||||
relative_path = "./executor-relative"
|
||||
};
|
||||
assert_eq!(
|
||||
response,
|
||||
EnvironmentConfigReadResponse {
|
||||
user_home_dir: dirs::home_dir()
|
||||
.and_then(|home_dir| PathUri::from_host_native_path(home_dir).ok()),
|
||||
codex_home_dir: PathUri::from_abs_path(&codex_home),
|
||||
hostname: codex_config::host_name(),
|
||||
config: EnvironmentConfigLayerStack {
|
||||
layers: vec![EnvironmentConfigLayer {
|
||||
source: format_config_layer_source(
|
||||
&ConfigLayerSource::Project {
|
||||
dot_codex_folder: dot_codex.clone(),
|
||||
},
|
||||
CONFIG_TOML_FILE,
|
||||
),
|
||||
base_dir: PathUri::from_abs_path(&dot_codex),
|
||||
toml: toml::to_string(&projected_toml)?,
|
||||
}],
|
||||
cloud_insertion_index: 0,
|
||||
},
|
||||
requirements: EnvironmentConfigLayerStack {
|
||||
layers: Vec::new(),
|
||||
cloud_insertion_index: 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn environment_config_read_rejects_empty_selectors() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
let codex_home =
|
||||
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(server.codex_home())?)?;
|
||||
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
|
||||
|
||||
for (config_paths, expected_message) in [
|
||||
(
|
||||
Vec::new(),
|
||||
"at least one config or requirements path is required",
|
||||
),
|
||||
(
|
||||
vec![Vec::new()],
|
||||
"TOML paths must contain at least one key segment",
|
||||
),
|
||||
] {
|
||||
let error = environment
|
||||
.read_environment_config(EnvironmentConfigReadParams {
|
||||
cwd: PathUri::from_abs_path(&codex_home),
|
||||
config_paths,
|
||||
requirements_paths: Vec::new(),
|
||||
})
|
||||
.await
|
||||
.expect_err("invalid selectors should fail");
|
||||
assert!(
|
||||
matches!(
|
||||
error,
|
||||
ExecServerError::Server { code: -32602, ref message }
|
||||
if message == expected_message
|
||||
),
|
||||
"unexpected error: {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user