mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Allow disabling the in-process code-mode host fallback (#35266)
## What changed - Allow `features.code_mode_host` to use a configuration table with `disable_in_process_fallback`. When enabled, failure to start the standalone host is returned as tool output instead of falling back to embedded V8. - Preserve the existing fallback behavior by default and continue accepting the boolean feature toggle. - Limit displayed host paths in spawn errors to 512 bytes while retaining the executable-bearing suffix and valid UTF-8 boundaries. ## Testing - Cover boolean and table-based feature configuration, fallback-disabled host failures, and bounded ASCII and UTF-8 error paths. GitOrigin-RevId: ab3d014e79054c2f8beef9a658915f01cca197b2
This commit is contained in:
committed by
copyberry
parent
a453588416
commit
cba0e2701c
@@ -38,6 +38,7 @@ type ShutdownResultReceiver = watch::Receiver<Option<Result<(), String>>>;
|
||||
/// Creates code-mode sessions backed by one lazily spawned process host.
|
||||
pub struct ProcessOwnedCodeModeSessionProvider {
|
||||
state: StdMutex<ProviderState>,
|
||||
allow_in_process_fallback: bool,
|
||||
}
|
||||
|
||||
/// Creates code-mode sessions backed by one shared remote WebSocket connection.
|
||||
@@ -56,9 +57,15 @@ impl ProcessOwnedCodeModeSessionProvider {
|
||||
state: StdMutex::new(ProviderState::OwnedProcess(Arc::new(
|
||||
OwnedCodeModeHost::new(host_program),
|
||||
))),
|
||||
allow_in_process_fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_in_process_fallback(mut self) -> Self {
|
||||
self.allow_in_process_fallback = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn process_host(&self) -> Option<Arc<OwnedCodeModeHost>> {
|
||||
match &*self
|
||||
.state
|
||||
@@ -91,7 +98,7 @@ impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
|
||||
|
||||
match process_host.connection().await {
|
||||
Ok(_) => {}
|
||||
Err(error) if error.host_program_not_found() => {
|
||||
Err(error) if error.host_program_not_found() && self.allow_in_process_fallback => {
|
||||
*self
|
||||
.state
|
||||
.lock()
|
||||
|
||||
@@ -58,6 +58,10 @@ mod transport;
|
||||
const IPC_CHANNEL_CAPACITY: usize = 128;
|
||||
const HOST_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_WEBSOCKET_FRAME_BYTES: usize = MAX_FRAME_BYTES + std::mem::size_of::<u32>();
|
||||
// Host spawn errors become model-visible tool output. Bound configured paths
|
||||
// while preserving the executable-bearing suffix needed to diagnose failures.
|
||||
const MAX_DISPLAYED_HOST_PROGRAM_BYTES: usize = 512;
|
||||
const TRUNCATED_HOST_PROGRAM_PREFIX: &str = "...";
|
||||
|
||||
pub(super) enum ConnectionError {
|
||||
Spawn {
|
||||
@@ -82,11 +86,27 @@ impl fmt::Display for ConnectionError {
|
||||
Self::Spawn {
|
||||
host_program,
|
||||
error,
|
||||
} => write!(
|
||||
formatter,
|
||||
"failed to spawn code-mode host {}: {error}",
|
||||
host_program.display()
|
||||
),
|
||||
} => {
|
||||
let host_program = host_program.to_string_lossy();
|
||||
if host_program.len() <= MAX_DISPLAYED_HOST_PROGRAM_BYTES {
|
||||
return write!(
|
||||
formatter,
|
||||
"failed to spawn code-mode host {host_program}: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut suffix_start = host_program.len()
|
||||
- (MAX_DISPLAYED_HOST_PROGRAM_BYTES - TRUNCATED_HOST_PROGRAM_PREFIX.len());
|
||||
while !host_program.is_char_boundary(suffix_start) {
|
||||
suffix_start += 1;
|
||||
}
|
||||
|
||||
write!(
|
||||
formatter,
|
||||
"failed to spawn code-mode host {TRUNCATED_HOST_PROGRAM_PREFIX}{}: {error}",
|
||||
&host_program[suffix_start..]
|
||||
)
|
||||
}
|
||||
Self::Other(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use tokio_tungstenite::tungstenite::Message;
|
||||
use super::ProcessOwnedCodeModeSession;
|
||||
use super::ProcessOwnedCodeModeSessionProvider;
|
||||
use super::WebSocketCodeModeSessionProvider;
|
||||
use super::connection::ConnectionError;
|
||||
use super::resolve_host_program;
|
||||
use crate::NoopCodeModeSessionDelegate;
|
||||
|
||||
@@ -93,6 +94,41 @@ fn host_program_falls_back_to_its_name_when_main_executable_is_unknown() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_host_error_limits_the_displayed_path_to_512_bytes() {
|
||||
let executable = "codex-code-mode-host-does-not-exist";
|
||||
let host_program = format!("{}{executable}", "missing-directory/".repeat(/*n*/ 64));
|
||||
let expected_suffix = &host_program[host_program.len() - (512 - "...".len())..];
|
||||
let error = ConnectionError::Spawn {
|
||||
host_program: PathBuf::from(&host_program),
|
||||
error: io::Error::new(io::ErrorKind::NotFound, "host unavailable"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
format!("failed to spawn code-mode host ...{expected_suffix}: host unavailable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_host_error_preserves_utf8_boundaries_when_truncating_the_path() {
|
||||
let executable = "codex-code-mode-host-does-not-exist";
|
||||
let host_program = format!("{}{executable}", "🦀".repeat(/*n*/ 256));
|
||||
let error = ConnectionError::Spawn {
|
||||
host_program: PathBuf::from(host_program),
|
||||
error: io::Error::new(io::ErrorKind::NotFound, "host unavailable"),
|
||||
}
|
||||
.to_string();
|
||||
let displayed_path = error
|
||||
.strip_prefix("failed to spawn code-mode host ")
|
||||
.and_then(|message| message.strip_suffix(": host unavailable"))
|
||||
.expect("missing-host error should contain the displayed host path");
|
||||
|
||||
assert!(displayed_path.starts_with("..."));
|
||||
assert!(displayed_path.ends_with(executable));
|
||||
assert!(displayed_path.len() <= 512);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_falls_back_to_in_process_session_when_host_is_missing() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
@@ -273,6 +309,23 @@ async fn websocket_provider_executes_over_shared_connector() {
|
||||
.expect("websocket test host task should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_returns_missing_host_error_when_in_process_fallback_is_disabled() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
"codex-code-mode-host-does-not-exist".into(),
|
||||
)
|
||||
.without_in_process_fallback();
|
||||
|
||||
let error = provider
|
||||
.create_session(Arc::new(NoopCodeModeSessionDelegate))
|
||||
.await
|
||||
.err()
|
||||
.expect("missing host should fail when in-process fallback is disabled");
|
||||
|
||||
assert!(error.contains("failed to spawn code-mode host codex-code-mode-host-does-not-exist"));
|
||||
assert!(provider.process_host().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_before_open_does_not_spawn_the_host() {
|
||||
let session = ProcessOwnedCodeModeSession::new();
|
||||
|
||||
@@ -35,6 +35,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if feature.id == codex_features::Feature::CodeModeHost {
|
||||
validation.properties.insert(
|
||||
feature.key.to_string(),
|
||||
schema_gen.subschema_for::<codex_features::FeatureToml<
|
||||
codex_features::CodeModeHostConfigToml,
|
||||
>>(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if feature.id == codex_features::Feature::NonPrefixedMcpToolNames {
|
||||
validation.properties.insert(
|
||||
feature.key.to_string(),
|
||||
|
||||
@@ -385,6 +385,19 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CodeModeHostConfigToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disable_in_process_fallback": {
|
||||
"description": "Fail instead of running embedded V8 when the standalone host is unavailable.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ConfigProfile": {
|
||||
"additionalProperties": false,
|
||||
"description": "Collection of common configuration options that a user can define as a unit in `config.toml`.",
|
||||
@@ -462,7 +475,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"code_mode_host": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/FeatureToml_for_CodeModeHostConfigToml"
|
||||
},
|
||||
"code_mode_only": {
|
||||
"type": "boolean"
|
||||
@@ -983,6 +996,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FeatureToml_for_CodeModeHostConfigToml": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CodeModeHostConfigToml"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FeatureToml_for_CurrentTimeReminderConfigToml": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -5002,7 +5025,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"code_mode_host": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/FeatureToml_for_CodeModeHostConfigToml"
|
||||
},
|
||||
"code_mode_only": {
|
||||
"type": "boolean"
|
||||
|
||||
@@ -537,6 +537,10 @@ async fn load_config_resolves_code_mode_config() -> std::io::Result<()> {
|
||||
enabled = true
|
||||
excluded_tool_namespaces = ["mcp__codex_apps", "multi_agent_v1"]
|
||||
direct_only_tool_namespaces = ["mcp__history", "mcp__notes"]
|
||||
|
||||
[features.code_mode_host]
|
||||
enabled = true
|
||||
disable_in_process_fallback = true
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
@@ -555,7 +559,9 @@ direct_only_tool_namespaces = ["mcp__history", "mcp__notes"]
|
||||
config.code_mode.direct_only_tool_namespaces,
|
||||
vec!["mcp__history".to_string(), "mcp__notes".to_string()]
|
||||
);
|
||||
assert!(config.code_mode.disable_in_process_fallback);
|
||||
assert!(config.features.enabled(Feature::CodeMode));
|
||||
assert!(config.features.enabled(Feature::CodeModeHost));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1099,6 +1099,7 @@ pub struct Config {
|
||||
pub struct CodeModeConfig {
|
||||
pub excluded_tool_namespaces: Vec<String>,
|
||||
pub direct_only_tool_namespaces: Vec<String>,
|
||||
pub disable_in_process_fallback: bool,
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE: &str = concat!(
|
||||
@@ -2556,6 +2557,14 @@ fn resolve_orchestrator_feature_enabled(
|
||||
|
||||
fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
|
||||
let base = code_mode_toml_config(config_toml.features.as_ref());
|
||||
let host = config_toml
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.code_mode_host.as_ref())
|
||||
.and_then(|feature| match feature {
|
||||
FeatureToml::Enabled(_) => None,
|
||||
FeatureToml::Config(config) => Some(config),
|
||||
});
|
||||
|
||||
CodeModeConfig {
|
||||
excluded_tool_namespaces: base
|
||||
@@ -2566,6 +2575,9 @@ fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
|
||||
.and_then(|config| config.direct_only_tool_namespaces.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
disable_in_process_fallback: host
|
||||
.and_then(|config| config.disable_in_process_fallback)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,9 @@ pub fn auth_manager_from_auth_with_home(auth: CodexAuth, codex_home: PathBuf) ->
|
||||
pub fn with_code_mode_host_program(
|
||||
thread_manager: ThreadManager,
|
||||
host_program: PathBuf,
|
||||
config: &crate::config::Config,
|
||||
) -> ThreadManager {
|
||||
thread_manager.with_code_mode_host_program_for_tests(host_program)
|
||||
thread_manager.with_code_mode_host_program_for_tests(host_program, config)
|
||||
}
|
||||
|
||||
pub fn thread_manager_with_models_provider(
|
||||
|
||||
@@ -374,6 +374,17 @@ impl ThreadManager {
|
||||
config.bundled_skills_enabled(),
|
||||
restriction_product,
|
||||
));
|
||||
let code_mode_session_provider: Arc<dyn CodeModeSessionProvider> =
|
||||
if config.features.enabled(Feature::CodeModeHost) {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::default();
|
||||
if config.code_mode.disable_in_process_fallback {
|
||||
Arc::new(provider.without_in_process_fallback())
|
||||
} else {
|
||||
Arc::new(provider)
|
||||
}
|
||||
} else {
|
||||
Arc::new(InProcessCodeModeSessionProvider)
|
||||
};
|
||||
Self {
|
||||
state: Arc::new(ThreadManagerState {
|
||||
threads: Arc::new(RwLock::new(HashMap::new())),
|
||||
@@ -384,11 +395,7 @@ impl ThreadManager {
|
||||
skills_service,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
code_mode_session_provider: if config.features.enabled(Feature::CodeModeHost) {
|
||||
Arc::new(ProcessOwnedCodeModeSessionProvider::default())
|
||||
} else {
|
||||
Arc::new(InProcessCodeModeSessionProvider)
|
||||
},
|
||||
code_mode_session_provider,
|
||||
extensions,
|
||||
user_instructions_provider,
|
||||
thread_store,
|
||||
@@ -418,13 +425,20 @@ impl ThreadManager {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_code_mode_host_program_for_tests(mut self, host_program: PathBuf) -> Self {
|
||||
pub(crate) fn with_code_mode_host_program_for_tests(
|
||||
mut self,
|
||||
host_program: PathBuf,
|
||||
config: &Config,
|
||||
) -> Self {
|
||||
let Some(state) = Arc::get_mut(&mut self.state) else {
|
||||
unreachable!("new thread manager state should not be shared");
|
||||
};
|
||||
state.code_mode_session_provider = Arc::new(
|
||||
ProcessOwnedCodeModeSessionProvider::with_host_program(host_program),
|
||||
);
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(host_program);
|
||||
state.code_mode_session_provider = if config.code_mode.disable_in_process_fallback {
|
||||
Arc::new(provider.without_in_process_fallback())
|
||||
} else {
|
||||
Arc::new(provider)
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -650,6 +650,7 @@ impl TestCodexBuilder {
|
||||
codex_core::test_support::with_code_mode_host_program(
|
||||
thread_manager,
|
||||
code_mode_host_program,
|
||||
&config,
|
||||
)
|
||||
} else {
|
||||
thread_manager
|
||||
|
||||
@@ -259,6 +259,63 @@ async fn missing_process_host_falls_back_to_in_process_code_mode() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_process_host_fails_when_in_process_fallback_is_disabled() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let builder = test_codex()
|
||||
.with_model("test-gpt-5.1-codex")
|
||||
.with_code_mode_host_program("codex-code-mode-host-does-not-exist".into())
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodeMode)
|
||||
.expect("code mode should be enabled");
|
||||
config.code_mode.disable_in_process_fallback = true;
|
||||
});
|
||||
let (_test, follow_up_mock) =
|
||||
run_code_mode_turn_with_builder(&server, "Run code mode", "text('unreachable')", builder)
|
||||
.await?;
|
||||
|
||||
let (output, _) =
|
||||
custom_tool_output_body_and_success(&follow_up_mock.single_request(), "call-1");
|
||||
assert!(output.contains("failed to spawn code-mode host codex-code-mode-host-does-not-exist"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_process_host_error_is_bounded_when_in_process_fallback_is_disabled() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let executable = "codex-code-mode-host-does-not-exist";
|
||||
let host_program = format!("{}{executable}", "missing-directory/".repeat(/*n*/ 64));
|
||||
let builder = test_codex()
|
||||
.with_model("test-gpt-5.1-codex")
|
||||
.with_code_mode_host_program(host_program.into())
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodeMode)
|
||||
.expect("code mode should be enabled");
|
||||
config.code_mode.disable_in_process_fallback = true;
|
||||
});
|
||||
let (_test, follow_up_mock) =
|
||||
run_code_mode_turn_with_builder(&server, "Run code mode", "text('unreachable')", builder)
|
||||
.await?;
|
||||
|
||||
let (output, _) =
|
||||
custom_tool_output_body_and_success(&follow_up_mock.single_request(), "call-1");
|
||||
assert!(output.contains("failed to spawn code-mode host ..."));
|
||||
assert!(output.contains(executable));
|
||||
assert!(output.len() <= 1024, "host error must remain bounded");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_can_call_standalone_web_search() -> Result<()> {
|
||||
assert_code_mode_standalone_web_search(WebSearchMode::Live, serde_json::json!(true)).await
|
||||
|
||||
@@ -29,6 +29,26 @@ impl FeatureConfig for CodeModeConfigToml {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CodeModeHostConfigToml {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
/// Fail instead of running embedded V8 when the standalone host is unavailable.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_in_process_fallback: Option<bool>,
|
||||
}
|
||||
|
||||
impl FeatureConfig for CodeModeHostConfigToml {
|
||||
fn enabled(&self) -> Option<bool> {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = Some(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NonPrefixedMcpToolNamesConfigToml {
|
||||
|
||||
@@ -17,6 +17,7 @@ use toml::Table;
|
||||
mod feature_configs;
|
||||
mod legacy;
|
||||
pub use feature_configs::CodeModeConfigToml;
|
||||
pub use feature_configs::CodeModeHostConfigToml;
|
||||
pub use feature_configs::CurrentTimeReminderConfigToml;
|
||||
pub use feature_configs::CurrentTimeReminderDeliveryMode;
|
||||
pub use feature_configs::CurrentTimeSource;
|
||||
@@ -652,6 +653,8 @@ pub struct FeaturesToml {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub code_mode: Option<FeatureToml<CodeModeConfigToml>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub code_mode_host: Option<FeatureToml<CodeModeHostConfigToml>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub non_prefixed_mcp_tool_names: Option<FeatureToml<NonPrefixedMcpToolNamesConfigToml>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
|
||||
@@ -690,6 +693,9 @@ impl FeaturesToml {
|
||||
if let Some(enabled) = self.code_mode.as_ref().and_then(FeatureToml::enabled) {
|
||||
entries.insert(Feature::CodeMode.key().to_string(), enabled);
|
||||
}
|
||||
if let Some(enabled) = self.code_mode_host.as_ref().and_then(FeatureToml::enabled) {
|
||||
entries.insert(Feature::CodeModeHost.key().to_string(), enabled);
|
||||
}
|
||||
if let Some(enabled) = self
|
||||
.non_prefixed_mcp_tool_names
|
||||
.as_ref()
|
||||
@@ -723,6 +729,7 @@ impl FeaturesToml {
|
||||
self.clear_removed_compatibility_entries();
|
||||
let Self {
|
||||
code_mode,
|
||||
code_mode_host,
|
||||
non_prefixed_mcp_tool_names,
|
||||
multi_agent_v2,
|
||||
token_budget,
|
||||
@@ -739,6 +746,8 @@ impl FeaturesToml {
|
||||
let enabled = features.enabled(spec.id);
|
||||
if spec.id == Feature::CodeMode {
|
||||
materialize_resolved_feature_enabled(code_mode, enabled);
|
||||
} else if spec.id == Feature::CodeModeHost {
|
||||
materialize_resolved_feature_enabled(code_mode_host, enabled);
|
||||
} else if spec.id == Feature::NonPrefixedMcpToolNames {
|
||||
materialize_resolved_feature_enabled(non_prefixed_mcp_tool_names, enabled);
|
||||
} else if spec.id == Feature::MultiAgentV2 {
|
||||
|
||||
@@ -85,6 +85,42 @@ fn code_mode_only_requires_code_mode() {
|
||||
assert_eq!(features.enabled(Feature::CodeMode), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_host_feature_config_preserves_boolean_toggle() {
|
||||
let features: FeaturesToml =
|
||||
toml::from_str("code_mode_host = false").expect("features table should deserialize");
|
||||
|
||||
assert_eq!(features.code_mode_host, Some(FeatureToml::Enabled(false)));
|
||||
assert_eq!(
|
||||
features.entries(),
|
||||
BTreeMap::from([("code_mode_host".to_string(), false)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_host_feature_config_deserializes_fallback_setting() {
|
||||
let features: FeaturesToml = toml::from_str(
|
||||
r#"
|
||||
[code_mode_host]
|
||||
enabled = true
|
||||
disable_in_process_fallback = true
|
||||
"#,
|
||||
)
|
||||
.expect("features table should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
features.code_mode_host,
|
||||
Some(FeatureToml::Config(crate::CodeModeHostConfigToml {
|
||||
enabled: Some(true),
|
||||
disable_in_process_fallback: Some(true),
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
features.entries(),
|
||||
BTreeMap::from([("code_mode_host".to_string(), true)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_sources_ignores_removed_terminal_resize_reflow_feature_key() {
|
||||
let features_toml = FeaturesToml::from(BTreeMap::from([(
|
||||
@@ -495,6 +531,10 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
|
||||
features.enable(Feature::RespectSystemProxy);
|
||||
|
||||
let mut features_toml = FeaturesToml {
|
||||
code_mode_host: Some(FeatureToml::Config(crate::CodeModeHostConfigToml {
|
||||
enabled: Some(false),
|
||||
disable_in_process_fallback: Some(true),
|
||||
})),
|
||||
multi_agent_v2: Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml {
|
||||
enabled: Some(false),
|
||||
min_wait_timeout_ms: Some(2500),
|
||||
@@ -526,6 +566,13 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
|
||||
spec.key
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
features_toml.code_mode_host,
|
||||
Some(FeatureToml::Config(crate::CodeModeHostConfigToml {
|
||||
enabled: Some(true),
|
||||
disable_in_process_fallback: Some(true),
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
features_toml.multi_agent_v2,
|
||||
Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml {
|
||||
|
||||
Reference in New Issue
Block a user