mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Make local environment optional in EnvironmentManager (#23369)
## Summary - make `EnvironmentManager` local environment/runtime paths optional - simplify constructor surface around snapshot materialization - rename local env accessors to `require_local_environment` / `try_local_environment` ## Validation - devbox Bazel build for touched crate surfaces - `//codex-rs/exec-server:exec-server-unit-tests` - `//codex-rs/app-server-client:app-server-client-unit-tests` - filtered touched `//codex-rs/core:core-unit-tests` cases
This commit is contained in:
@@ -27,12 +27,11 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
///
|
||||
/// `EnvironmentManager` is a shared registry for concrete environments. Its
|
||||
/// default constructor preserves the legacy `CODEX_EXEC_SERVER_URL` behavior
|
||||
/// while provider-based construction accepts a provider-supplied snapshot.
|
||||
/// while configured construction accepts a provider-supplied snapshot.
|
||||
///
|
||||
/// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving
|
||||
/// the default environment unset while still keeping an explicit local
|
||||
/// environment available through `local_environment()`. Callers use
|
||||
/// `default_environment().is_some()` as the signal for model-facing
|
||||
/// the default environment unset and omitting the local environment. Callers
|
||||
/// use `default_environment().is_some()` as the signal for model-facing
|
||||
/// shell/filesystem tool availability.
|
||||
///
|
||||
/// Remote environments create remote filesystem and execution backends that
|
||||
@@ -42,25 +41,13 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
pub struct EnvironmentManager {
|
||||
default_environment: Option<String>,
|
||||
environments: RwLock<HashMap<String, Arc<Environment>>>,
|
||||
local_environment: Arc<Environment>,
|
||||
local_environment: Option<Arc<Environment>>,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
}
|
||||
|
||||
pub const LOCAL_ENVIRONMENT_ID: &str = "local";
|
||||
pub const REMOTE_ENVIRONMENT_ID: &str = "remote";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnvironmentManagerArgs {
|
||||
pub local_runtime_paths: ExecServerRuntimePaths,
|
||||
}
|
||||
|
||||
impl EnvironmentManagerArgs {
|
||||
pub fn new(local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
local_runtime_paths,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentManager {
|
||||
/// Builds a test-only manager without configured sandbox helper paths.
|
||||
pub fn default_for_tests() -> Self {
|
||||
@@ -70,37 +57,29 @@ impl EnvironmentManager {
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(Environment::default_for_tests()),
|
||||
)])),
|
||||
local_environment: Arc::new(Environment::default_for_tests()),
|
||||
local_environment: Some(Arc::new(Environment::default_for_tests())),
|
||||
local_runtime_paths: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a test-only manager with environment access disabled.
|
||||
pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
/// Builds a manager with no configured execution environments.
|
||||
pub fn without_environments() -> Self {
|
||||
Self {
|
||||
default_environment: None,
|
||||
environments: RwLock::new(HashMap::new()),
|
||||
local_environment: Arc::new(Environment::local(local_runtime_paths)),
|
||||
local_environment: None,
|
||||
local_runtime_paths: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a test-only manager from a raw exec-server URL value.
|
||||
pub async fn create_for_tests(
|
||||
exec_server_url: Option<String>,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
Self::from_default_provider_url(exec_server_url, local_runtime_paths).await
|
||||
}
|
||||
|
||||
/// Builds a manager from `CODEX_EXEC_SERVER_URL` and local runtime paths
|
||||
/// used when creating local filesystem helpers.
|
||||
pub async fn new(args: EnvironmentManagerArgs) -> Self {
|
||||
let EnvironmentManagerArgs {
|
||||
local_runtime_paths,
|
||||
} = args;
|
||||
let exec_server_url = std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok();
|
||||
Self::from_default_provider_url(exec_server_url, local_runtime_paths).await
|
||||
}
|
||||
|
||||
/// Builds a manager from `CODEX_HOME` and local runtime paths used when
|
||||
/// creating local filesystem helpers.
|
||||
///
|
||||
@@ -109,27 +88,27 @@ impl EnvironmentManager {
|
||||
/// `CODEX_EXEC_SERVER_URL` behavior.
|
||||
pub async fn from_codex_home(
|
||||
codex_home: impl AsRef<std::path::Path>,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let provider = environment_provider_from_codex_home(codex_home.as_ref())?;
|
||||
Self::from_provider(provider.as_ref(), local_runtime_paths).await
|
||||
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
|
||||
}
|
||||
|
||||
/// Builds a manager from the legacy environment-variable provider without
|
||||
/// reading user config files from `CODEX_HOME`.
|
||||
pub async fn from_env(
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let provider = DefaultEnvironmentProvider::from_env();
|
||||
Self::from_provider(&provider, local_runtime_paths).await
|
||||
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
|
||||
}
|
||||
|
||||
async fn from_default_provider_url(
|
||||
exec_server_url: Option<String>,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
let provider = DefaultEnvironmentProvider::new(exec_server_url);
|
||||
match Self::from_provider(&provider, local_runtime_paths).await {
|
||||
match Self::from_snapshot(provider.snapshot_inner(), local_runtime_paths) {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => panic!("default provider should create valid environments: {err}"),
|
||||
}
|
||||
@@ -143,26 +122,15 @@ impl EnvironmentManager {
|
||||
) -> Self {
|
||||
let mut snapshot = DefaultEnvironmentProvider::new(exec_server_url).snapshot_inner();
|
||||
snapshot.include_local = true;
|
||||
match Self::from_provider_snapshot(snapshot, local_runtime_paths) {
|
||||
match Self::from_snapshot(snapshot, Some(local_runtime_paths)) {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => panic!("test provider with local should create valid environments: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a manager from a provider-supplied startup snapshot.
|
||||
pub async fn from_provider<P>(
|
||||
provider: &P,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<Self, ExecServerError>
|
||||
where
|
||||
P: EnvironmentProvider + ?Sized,
|
||||
{
|
||||
Self::from_provider_snapshot(provider.snapshot().await?, local_runtime_paths)
|
||||
}
|
||||
|
||||
fn from_provider_snapshot(
|
||||
fn from_snapshot(
|
||||
snapshot: EnvironmentProviderSnapshot,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let EnvironmentProviderSnapshot {
|
||||
environments,
|
||||
@@ -171,13 +139,21 @@ impl EnvironmentManager {
|
||||
} = snapshot;
|
||||
let mut environment_map =
|
||||
HashMap::with_capacity(environments.len() + usize::from(include_local));
|
||||
let local_environment = Arc::new(Environment::local(local_runtime_paths));
|
||||
if include_local {
|
||||
let local_environment = if include_local {
|
||||
let local_runtime_paths = local_runtime_paths.clone().ok_or_else(|| {
|
||||
ExecServerError::Protocol(
|
||||
"local environment requires configured runtime paths".to_string(),
|
||||
)
|
||||
})?;
|
||||
let local_environment = Arc::new(Environment::local(local_runtime_paths));
|
||||
environment_map.insert(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::clone(&local_environment),
|
||||
);
|
||||
}
|
||||
Some(local_environment)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for (id, environment) in environments {
|
||||
if id.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
@@ -213,6 +189,7 @@ impl EnvironmentManager {
|
||||
default_environment,
|
||||
environments: RwLock::new(environment_map),
|
||||
local_environment,
|
||||
local_runtime_paths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,9 +225,15 @@ impl EnvironmentManager {
|
||||
environment_ids
|
||||
}
|
||||
|
||||
/// Returns the local environment instance used for internal runtime work.
|
||||
pub fn local_environment(&self) -> Arc<Environment> {
|
||||
Arc::clone(&self.local_environment)
|
||||
/// Returns the local environment instance when one is configured.
|
||||
pub fn try_local_environment(&self) -> Option<Arc<Environment>> {
|
||||
self.local_environment.as_ref().map(Arc::clone)
|
||||
}
|
||||
|
||||
/// Returns the default environment or local environment when either exists.
|
||||
pub fn default_or_local_environment(&self) -> Option<Arc<Environment>> {
|
||||
self.default_environment()
|
||||
.or_else(|| self.try_local_environment())
|
||||
}
|
||||
|
||||
/// Returns a named environment instance.
|
||||
@@ -285,10 +268,8 @@ impl EnvironmentManager {
|
||||
"remote environment requires an exec-server url".to_string(),
|
||||
));
|
||||
};
|
||||
let environment = Environment::remote_inner(
|
||||
exec_server_url,
|
||||
self.local_environment.local_runtime_paths.clone(),
|
||||
);
|
||||
let environment =
|
||||
Environment::remote_inner(exec_server_url, self.local_runtime_paths.clone());
|
||||
self.environments
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
@@ -452,25 +433,12 @@ mod tests {
|
||||
use super::EnvironmentManager;
|
||||
use super::LOCAL_ENVIRONMENT_ID;
|
||||
use super::REMOTE_ENVIRONMENT_ID;
|
||||
use crate::EnvironmentProvider;
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use crate::environment_provider::EnvironmentDefault;
|
||||
use crate::environment_provider::EnvironmentProviderSnapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
struct TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EnvironmentProvider for TestEnvironmentProvider {
|
||||
async fn snapshot(&self) -> Result<EnvironmentProviderSnapshot, ExecServerError> {
|
||||
Ok(self.snapshot.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
@@ -479,6 +447,10 @@ mod tests {
|
||||
.expect("runtime paths")
|
||||
}
|
||||
|
||||
fn assert_local_environment_unavailable(manager: &EnvironmentManager) {
|
||||
assert!(manager.try_local_environment().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_local_environment_does_not_connect() {
|
||||
let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths())
|
||||
@@ -491,7 +463,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_normalizes_empty_url() {
|
||||
let manager =
|
||||
EnvironmentManager::create_for_tests(Some(String::new()), test_runtime_paths()).await;
|
||||
EnvironmentManager::create_for_tests(Some(String::new()), Some(test_runtime_paths()))
|
||||
.await;
|
||||
|
||||
let environment = manager.default_environment().expect("default environment");
|
||||
assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
|
||||
@@ -501,18 +474,22 @@ mod tests {
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment")
|
||||
));
|
||||
assert!(Arc::ptr_eq(&environment, &manager.local_environment()));
|
||||
assert!(Arc::ptr_eq(
|
||||
&environment,
|
||||
&manager.try_local_environment().expect("local environment")
|
||||
));
|
||||
assert!(manager.try_local_environment().is_some());
|
||||
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
||||
assert!(!environment.is_remote());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_environment_manager_has_no_default_but_keeps_explicit_local_environment() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
async fn disabled_environment_manager_has_no_default_or_local_environment() {
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
|
||||
assert!(manager.default_environment().is_none());
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
assert!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
||||
}
|
||||
@@ -521,7 +498,7 @@ mod tests {
|
||||
async fn environment_manager_reports_remote_url() {
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
test_runtime_paths(),
|
||||
Some(test_runtime_paths()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -539,7 +516,7 @@ mod tests {
|
||||
.expect("remote environment")
|
||||
));
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -557,20 +534,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_builds_from_provider() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()),
|
||||
include_local: false,
|
||||
},
|
||||
async fn environment_manager_builds_from_snapshot() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()),
|
||||
include_local: false,
|
||||
};
|
||||
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect("environment manager");
|
||||
|
||||
assert_eq!(
|
||||
@@ -584,20 +558,17 @@ mod tests {
|
||||
.is_remote()
|
||||
);
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_empty_environment_id() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![("".to_string(), Environment::default_for_tests())],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![("".to_string(), Environment::default_for_tests())],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
};
|
||||
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect_err("empty id should fail");
|
||||
|
||||
assert_eq!(
|
||||
@@ -608,18 +579,15 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_provider_supplied_local_environment() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Environment::default_for_tests(),
|
||||
)],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Environment::default_for_tests(),
|
||||
)],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
};
|
||||
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect_err("local id should fail");
|
||||
|
||||
assert_eq!(
|
||||
@@ -630,19 +598,16 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_uses_explicit_provider_default() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
"devbox".to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::EnvironmentId("devbox".to_string()),
|
||||
include_local: true,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
"devbox".to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::EnvironmentId("devbox".to_string()),
|
||||
include_local: true,
|
||||
};
|
||||
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect("manager");
|
||||
|
||||
assert_eq!(manager.default_environment_id(), Some("devbox"));
|
||||
@@ -655,19 +620,16 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_disables_provider_default() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
"devbox".to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: true,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
"devbox".to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: true,
|
||||
};
|
||||
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect("manager");
|
||||
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
@@ -676,25 +638,22 @@ mod tests {
|
||||
&manager
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment"),
|
||||
&manager.local_environment()
|
||||
&manager.try_local_environment().expect("local environment")
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_unknown_provider_default() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
"devbox".to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::EnvironmentId("missing".to_string()),
|
||||
include_local: true,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
"devbox".to_string(),
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
)],
|
||||
default: EnvironmentDefault::EnvironmentId("missing".to_string()),
|
||||
include_local: true,
|
||||
};
|
||||
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect_err("unknown default should fail");
|
||||
|
||||
assert_eq!(
|
||||
@@ -707,7 +666,7 @@ mod tests {
|
||||
async fn environment_manager_includes_local_for_default_provider_without_url() {
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
/*exec_server_url*/ None,
|
||||
test_runtime_paths(),
|
||||
Some(test_runtime_paths()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -719,7 +678,10 @@ mod tests {
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment")
|
||||
));
|
||||
assert!(Arc::ptr_eq(&environment, &manager.local_environment()));
|
||||
assert!(Arc::ptr_eq(
|
||||
&environment,
|
||||
&manager.try_local_environment().expect("local environment")
|
||||
));
|
||||
assert!(!environment.is_remote());
|
||||
}
|
||||
|
||||
@@ -728,44 +690,59 @@ mod tests {
|
||||
let runtime_paths = test_runtime_paths();
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
/*exec_server_url*/ None,
|
||||
runtime_paths.clone(),
|
||||
Some(runtime_paths.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let environment = manager.local_environment();
|
||||
let environment = manager.try_local_environment().expect("local environment");
|
||||
|
||||
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
environment.exec_server_url().map(str::to_owned),
|
||||
environment
|
||||
.local_runtime_paths()
|
||||
.expect("local runtime paths")
|
||||
.clone(),
|
||||
Some(
|
||||
environment
|
||||
.local_runtime_paths()
|
||||
.expect("local runtime paths")
|
||||
.clone(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let environment = manager.local_environment();
|
||||
let environment = manager.try_local_environment().expect("local environment");
|
||||
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_environment_manager_has_no_default_environment() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
|
||||
assert!(manager.default_environment().is_none());
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_omits_default_provider_local_lookup_when_default_disabled() {
|
||||
let manager =
|
||||
EnvironmentManager::create_for_tests(Some("none".to_string()), test_runtime_paths())
|
||||
.await;
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
Some("none".to_string()),
|
||||
Some(test_runtime_paths()),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(manager.default_environment().is_none());
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
||||
assert!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_snapshot_without_local_environment_disables_local_default() {
|
||||
let mut snapshot = EnvironmentProviderSnapshot {
|
||||
environments: Vec::new(),
|
||||
default: EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()),
|
||||
include_local: true,
|
||||
};
|
||||
snapshot.include_local = false;
|
||||
snapshot.default = EnvironmentDefault::Disabled;
|
||||
let manager =
|
||||
EnvironmentManager::from_snapshot(snapshot, /*local_runtime_paths*/ None)
|
||||
.expect("environment manager");
|
||||
|
||||
assert!(manager.default_environment().is_none());
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -777,7 +754,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_upserts_named_remote_environment() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
|
||||
manager
|
||||
.upsert_environment("executor-a".to_string(), "ws://127.0.0.1:8765".to_string())
|
||||
@@ -802,7 +779,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_empty_remote_environment_url() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
|
||||
let err = manager
|
||||
.upsert_environment("executor-a".to_string(), String::new())
|
||||
|
||||
@@ -27,6 +27,7 @@ const MAX_ENVIRONMENT_ID_LEN: usize = 64;
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EnvironmentsToml {
|
||||
default: Option<String>,
|
||||
include_local: Option<bool>,
|
||||
|
||||
#[serde(default)]
|
||||
environments: Vec<EnvironmentToml>,
|
||||
@@ -50,6 +51,7 @@ struct EnvironmentToml {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TomlEnvironmentProvider {
|
||||
default: EnvironmentDefault,
|
||||
include_local: bool,
|
||||
environments: Vec<(String, ExecServerTransportParams)>,
|
||||
}
|
||||
|
||||
@@ -63,21 +65,31 @@ impl TomlEnvironmentProvider {
|
||||
config: EnvironmentsToml,
|
||||
config_dir: Option<&Path>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let mut ids = HashSet::from([LOCAL_ENVIRONMENT_ID.to_string()]);
|
||||
let mut environments = Vec::with_capacity(config.environments.len());
|
||||
for item in config.environments {
|
||||
let EnvironmentsToml {
|
||||
default,
|
||||
include_local,
|
||||
environments,
|
||||
} = config;
|
||||
let include_local = include_local.unwrap_or(true);
|
||||
let mut ids = HashSet::new();
|
||||
if include_local {
|
||||
ids.insert(LOCAL_ENVIRONMENT_ID.to_string());
|
||||
}
|
||||
let mut parsed_environments = Vec::with_capacity(environments.len());
|
||||
for item in environments {
|
||||
let (id, transport) = parse_environment_toml(item, config_dir)?;
|
||||
if !ids.insert(id.clone()) {
|
||||
return Err(ExecServerError::Protocol(format!(
|
||||
"environment id `{id}` is duplicated"
|
||||
)));
|
||||
}
|
||||
environments.push((id, transport));
|
||||
parsed_environments.push((id, transport));
|
||||
}
|
||||
let default = normalize_default_environment_id(config.default.as_deref(), &ids)?;
|
||||
let default = normalize_default_environment_id(default.as_deref(), include_local, &ids)?;
|
||||
Ok(Self {
|
||||
default,
|
||||
environments,
|
||||
include_local,
|
||||
environments: parsed_environments,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -99,7 +111,7 @@ impl EnvironmentProvider for TomlEnvironmentProvider {
|
||||
Ok(EnvironmentProviderSnapshot {
|
||||
environments,
|
||||
default: self.default.clone(),
|
||||
include_local: true,
|
||||
include_local: self.include_local,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -212,12 +224,17 @@ pub(crate) fn environment_provider_from_codex_home(
|
||||
|
||||
fn normalize_default_environment_id(
|
||||
default: Option<&str>,
|
||||
include_local: bool,
|
||||
ids: &HashSet<String>,
|
||||
) -> Result<EnvironmentDefault, ExecServerError> {
|
||||
let Some(default) = default.map(str::trim) else {
|
||||
return Ok(EnvironmentDefault::EnvironmentId(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
));
|
||||
return if include_local {
|
||||
Ok(EnvironmentDefault::EnvironmentId(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(EnvironmentDefault::Disabled)
|
||||
};
|
||||
};
|
||||
if default.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
@@ -330,6 +347,7 @@ mod tests {
|
||||
async fn toml_provider_includes_local_and_adds_configured_environments() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("ssh-dev".to_string()),
|
||||
include_local: None,
|
||||
environments: vec![
|
||||
EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
@@ -396,6 +414,7 @@ mod tests {
|
||||
async fn toml_provider_default_none_disables_default() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("none".to_string()),
|
||||
include_local: None,
|
||||
environments: Vec::new(),
|
||||
})
|
||||
.expect("provider");
|
||||
@@ -405,6 +424,55 @@ mod tests {
|
||||
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toml_provider_can_disable_local_environment() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("ssh-dev".to_string()),
|
||||
include_local: Some(false),
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "ssh-dev".to_string(),
|
||||
program: Some("ssh".to_string()),
|
||||
..Default::default()
|
||||
}],
|
||||
})
|
||||
.expect("provider");
|
||||
let snapshot = provider.snapshot().await.expect("environments");
|
||||
|
||||
assert!(!snapshot.include_local);
|
||||
assert_eq!(
|
||||
snapshot.default,
|
||||
EnvironmentDefault::EnvironmentId("ssh-dev".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toml_provider_without_local_and_default_omitted_disables_default() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
include_local: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("provider");
|
||||
let snapshot = provider.snapshot().await.expect("environments");
|
||||
|
||||
assert!(!snapshot.include_local);
|
||||
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_provider_rejects_local_default_when_local_is_disabled() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some(LOCAL_ENVIRONMENT_ID.to_string()),
|
||||
include_local: Some(false),
|
||||
environments: Vec::new(),
|
||||
})
|
||||
.expect_err("local default without local environment should fail");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec-server protocol error: default environment `local` is not configured"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_provider_rejects_invalid_environments() {
|
||||
let cases = [
|
||||
@@ -479,6 +547,7 @@ mod tests {
|
||||
for (item, expected) in cases {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![item],
|
||||
})
|
||||
.expect_err("invalid item should fail");
|
||||
@@ -496,6 +565,7 @@ mod tests {
|
||||
let provider = TomlEnvironmentProvider::new_with_config_dir(
|
||||
EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "ssh-dev".to_string(),
|
||||
program: Some("ssh".to_string()),
|
||||
@@ -525,6 +595,7 @@ mod tests {
|
||||
fn toml_provider_parses_configured_transport_timeouts() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![
|
||||
EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
@@ -569,6 +640,7 @@ mod tests {
|
||||
fn toml_provider_rejects_relative_stdio_cwd_without_config_dir() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "ssh-dev".to_string(),
|
||||
program: Some("ssh".to_string()),
|
||||
@@ -588,6 +660,7 @@ mod tests {
|
||||
fn toml_provider_rejects_duplicate_ids() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![
|
||||
EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
@@ -614,6 +687,7 @@ mod tests {
|
||||
let id = "a".repeat(MAX_ENVIRONMENT_ID_LEN + 1);
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: id.clone(),
|
||||
url: Some("ws://127.0.0.1:8765".to_string()),
|
||||
@@ -634,6 +708,7 @@ mod tests {
|
||||
fn toml_provider_rejects_unknown_default() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("missing".to_string()),
|
||||
include_local: None,
|
||||
environments: Vec::new(),
|
||||
})
|
||||
.expect_err("unknown default should fail");
|
||||
@@ -652,6 +727,7 @@ mod tests {
|
||||
&path,
|
||||
r#"
|
||||
default = "ssh-dev"
|
||||
include_local = false
|
||||
|
||||
[[environments]]
|
||||
id = "devbox"
|
||||
@@ -673,6 +749,7 @@ CODEX_LOG = "debug"
|
||||
let environments = load_environments_toml(&path).expect("environments.toml");
|
||||
|
||||
assert_eq!(environments.default.as_deref(), Some("ssh-dev"));
|
||||
assert_eq!(environments.include_local, Some(false));
|
||||
assert_eq!(environments.environments.len(), 2);
|
||||
assert_eq!(
|
||||
environments.environments[0],
|
||||
@@ -736,6 +813,7 @@ unknown = true
|
||||
fn toml_provider_rejects_malformed_websocket_url() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
url: Some("ws://".to_string()),
|
||||
@@ -758,6 +836,7 @@ unknown = true
|
||||
codex_home.path().join(ENVIRONMENTS_TOML_FILE),
|
||||
r#"
|
||||
default = "none"
|
||||
include_local = false
|
||||
"#,
|
||||
)
|
||||
.expect("write environments.toml");
|
||||
@@ -772,7 +851,7 @@ default = "none"
|
||||
.map(|(id, _environment)| id)
|
||||
.collect();
|
||||
|
||||
assert!(snapshot.include_local);
|
||||
assert!(!snapshot.include_local);
|
||||
assert!(!environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string()));
|
||||
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user