fix(core/config): coerce auto upgrade booleans

Closes #219
This commit is contained in:
James Peter
2025-09-22 20:43:18 +10:00
parent f527c1d17f
commit 158cb551c9
3 changed files with 77 additions and 3 deletions
+51
View File
@@ -38,6 +38,7 @@ use codex_protocol::mcp_protocol::AuthMode;
use codex_protocol::config_types::SandboxMode;
use dirs::home_dir;
use serde::Deserialize;
use serde::de::{self, Unexpected};
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::ErrorKind;
@@ -1345,6 +1346,7 @@ pub struct ConfigToml {
pub disable_response_storage: Option<bool>,
/// Enable silent upgrades during startup when a newer release is available.
#[serde(default, deserialize_with = "deserialize_option_bool_from_maybe_string")]
pub auto_upgrade_enabled: Option<bool>,
/// Optional external command to spawn for end-user notifications.
@@ -1449,6 +1451,37 @@ pub struct ConfigToml {
pub experimental_resume: Option<PathBuf>,
}
fn deserialize_option_bool_from_maybe_string<'de, D>(
deserializer: D,
) -> Result<Option<bool>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum BoolOrString {
Bool(bool),
String(String),
}
let value = Option::<BoolOrString>::deserialize(deserializer)?;
match value {
Some(BoolOrString::Bool(b)) => Ok(Some(b)),
Some(BoolOrString::String(s)) => {
let normalized = s.trim().to_ascii_lowercase();
match normalized.as_str() {
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
_ => Err(de::Error::invalid_value(
Unexpected::Str(&s),
&"a boolean or string 'true'/'false'",
)),
}
}
None => Ok(None),
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct ProjectConfig {
pub trust_level: Option<String>,
@@ -2158,6 +2191,24 @@ persistence = "none"
);
}
#[test]
fn auto_upgrade_enabled_accepts_string_boolean() {
let cfg_true = r#"auto_upgrade_enabled = "true""#;
let parsed_true = toml::from_str::<ConfigToml>(cfg_true)
.expect("string boolean should deserialize");
assert_eq!(parsed_true.auto_upgrade_enabled, Some(true));
let cfg_false = r#"auto_upgrade_enabled = "false""#;
let parsed_false = toml::from_str::<ConfigToml>(cfg_false)
.expect("string boolean should deserialize");
assert_eq!(parsed_false.auto_upgrade_enabled, Some(false));
let cfg_bool = r#"auto_upgrade_enabled = true"#;
let parsed_bool = toml::from_str::<ConfigToml>(cfg_bool)
.expect("boolean should deserialize");
assert_eq!(parsed_bool.auto_upgrade_enabled, Some(true));
}
#[test]
fn tui_config_missing_notifications_field_defaults_to_disabled() {
let cfg = r#"
+25 -1
View File
@@ -367,7 +367,11 @@ async fn persist_overrides_with_behavior(
}
match value {
Some(v) => {
let item_value = toml_edit::value(v);
let trimmed = v.trim();
let item_value = match trimmed.parse::<bool>() {
Ok(parsed_bool) => toml_edit::value(parsed_bool),
Err(_) => toml_edit::value(v),
};
apply_toml_edit_override_segments(&mut doc, segments_to_apply, item_value);
mutated = true;
}
@@ -550,6 +554,26 @@ model = "gpt-5"
assert_eq!(contents, expected);
}
#[tokio::test]
async fn persist_overrides_writes_boolean_literals() {
let tmpdir = tempdir().expect("tmp");
let codex_home = tmpdir.path();
persist_overrides(codex_home, None, &[(&["auto_upgrade_enabled"], "true")])
.await
.expect("persist");
let contents = read_config(codex_home).await;
assert!(contents.contains("auto_upgrade_enabled = true"));
persist_overrides(codex_home, None, &[(&["auto_upgrade_enabled"], "false")])
.await
.expect("persist");
let contents = read_config(codex_home).await;
assert!(contents.contains("auto_upgrade_enabled = false"));
}
/// Verifies a scalar key becomes a table when nested keys are written.
#[tokio::test]
async fn persist_overrides_replaces_scalar_with_table() {
+1 -2
View File
@@ -7364,11 +7364,10 @@ fn update_rate_limit_resets(
let codex_home = self.config.codex_home.clone();
let profile = self.config.active_profile.clone();
tokio::spawn(async move {
let value = if enabled { "true".to_string() } else { "false".to_string() };
if let Err(err) = codex_core::config_edit::persist_overrides(
&codex_home,
profile.as_deref(),
&[(&["auto_upgrade_enabled"], value.as_str())],
&[(&["auto_upgrade_enabled"], if enabled { "true" } else { "false" })],
)
.await
{