From 5b682c987528bb3be560ef75f3759ae89160d949 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 8 Sep 2026 23:52:48 +0000 Subject: [PATCH] Show configured app-server updater settings in doctor (#43948) ## What changed Report the automatic-update setting and update interval from the daemon's `settings.json` in the background-server check, labeling each value as configured. Report unreadable, invalid, or oversized settings files without displaying their values, with a 16 KiB size limit. ## Testing Add snapshot coverage for disabled automatic updates and a configured interval when the updater PID file is missing, plus invalid boolean and zero-interval settings. GitOrigin-RevId: e8e77d1a10448f7d62fd84d8325f3e61738a689b --- codex-rs/cli/src/doctor/background.rs | 95 +++++++++++++++++++ ...background__tests__configured_updater.snap | 8 ++ 2 files changed, 103 insertions(+) create mode 100644 codex-rs/cli/src/doctor/snapshots/codex__doctor__background__tests__configured_updater.snap diff --git a/codex-rs/cli/src/doctor/background.rs b/codex-rs/cli/src/doctor/background.rs index 3611e28f6c..7a4dc516c3 100644 --- a/codex-rs/cli/src/doctor/background.rs +++ b/codex-rs/cli/src/doctor/background.rs @@ -6,9 +6,12 @@ //! keeps doctor safe to run while the user is debugging startup or update-loop //! issues. +use std::io::Read; +use std::num::NonZeroU32; use std::path::Path; use codex_core::config::Config; +use serde::Deserialize; use super::CheckStatus; use super::DoctorCheck; @@ -18,6 +21,19 @@ const STATE_DIR_NAME: &str = "app-server-daemon"; const SETTINGS_FILE_NAME: &str = "settings.json"; const PID_FILE_NAME: &str = "app-server.pid"; const UPDATE_PID_FILE_NAME: &str = "app-server-updater.pid"; +const MAX_SETTINGS_BYTES: u64 = 16 * 1024; + +#[derive(Deserialize)] +struct ConfiguredSettings { + updater: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ConfiguredUpdater { + auto_update_enabled: Option, + update_interval_minutes: Option, +} /// Builds the app-server status row from existing daemon state. /// @@ -39,6 +55,7 @@ pub(super) async fn background_server_check(config: &Config) -> DoctorCheck { "update-loop pid file", &state_dir.join(UPDATE_PID_FILE_NAME), ); + push_configured_updater(&mut details, &state_dir.join(SETTINGS_FILE_NAME)); let socket_path = match codex_app_server::app_server_control_socket_path(&config.codex_home) { Ok(socket_path) => socket_path, @@ -90,6 +107,34 @@ fn push_file_detail(details: &mut Vec, label: &str, path: &Path) { } } +fn push_configured_updater(details: &mut Vec, path: &Path) { + if !path.is_file() { + return; + } + let mut contents = Vec::new(); + let read = std::fs::File::open(path) + .and_then(|file| file.take(MAX_SETTINGS_BYTES + 1).read_to_end(&mut contents)); + let settings = read + .ok() + .filter(|_| contents.len() as u64 <= MAX_SETTINGS_BYTES) + .and_then(|_| serde_json::from_slice::(&contents).ok()); + let Some(settings) = settings else { + details.push("configured updater settings: unreadable or invalid".into()); + return; + }; + if let Some(updater) = settings.updater { + if let Some(enabled) = updater.auto_update_enabled { + details.push(format!( + "automatic updates: {} (configured)", + if enabled { "enabled" } else { "disabled" } + )); + } + if let Some(minutes) = updater.update_interval_minutes { + details.push(format!("update interval: {minutes} minutes (configured)")); + } + } +} + fn server_mode(state_dir: &Path) -> &'static str { if state_dir.join(SETTINGS_FILE_NAME).is_file() { "persistent" @@ -256,4 +301,54 @@ mod tests { .any(|detail| detail.starts_with("app-server version: unavailable (")) ); } + + #[tokio::test] + async fn configured_updates_and_missing_updater_record_are_visible() { + let temp = tempfile::tempdir().expect("tempdir"); + let state_dir = temp.path().join(STATE_DIR_NAME); + std::fs::create_dir(&state_dir).expect("state dir"); + std::fs::write( + state_dir.join(SETTINGS_FILE_NAME), + r#"{"updater":{"autoUpdateEnabled":false,"updateIntervalMinutes":90}}"#, + ) + .expect("settings"); + let config = test_config(temp.path().to_path_buf()).await; + + let check = background_server_check(&config).await; + let details = check + .details + .iter() + .filter(|detail| { + detail.starts_with("automatic updates:") + || detail.starts_with("update interval:") + || detail.starts_with("update-loop pid file:") + }) + .map(|detail| { + detail + .replace(&temp.path().display().to_string(), "CODEX_HOME") + .replace('\\', "/") + }) + .collect::>() + .join("\n"); + insta::assert_snapshot!("configured_updater", details); + } + + #[test] + fn invalid_settings_are_not_reported_as_effective() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join(SETTINGS_FILE_NAME); + for contents in [ + r#"{"updater":{"autoUpdateEnabled":"no"}}"#, + r#"{"updater":{"updateIntervalMinutes":0}}"#, + ] { + std::fs::write(&path, contents).expect("settings"); + let mut details = Vec::new(); + + push_configured_updater(&mut details, &path); + + insta::allow_duplicates! { + insta::assert_snapshot!(details.join("\n"), @"configured updater settings: unreadable or invalid"); + } + } + } } diff --git a/codex-rs/cli/src/doctor/snapshots/codex__doctor__background__tests__configured_updater.snap b/codex-rs/cli/src/doctor/snapshots/codex__doctor__background__tests__configured_updater.snap new file mode 100644 index 0000000000..63f0df4058 --- /dev/null +++ b/codex-rs/cli/src/doctor/snapshots/codex__doctor__background__tests__configured_updater.snap @@ -0,0 +1,8 @@ +--- +source: cli/src/doctor/background.rs +assertion_line: 332 +expression: details +--- +update-loop pid file: CODEX_HOME/app-server-daemon/app-server-updater.pid (missing) +automatic updates: disabled (configured) +update interval: 90 minutes (configured)