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
This commit is contained in:
Eric Traut
2026-09-09 00:10:54 +00:00
committed by copyberry
parent 5ac0b8768d
commit 5b682c9875
2 changed files with 103 additions and 0 deletions
+95
View File
@@ -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<ConfiguredUpdater>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConfiguredUpdater {
auto_update_enabled: Option<bool>,
update_interval_minutes: Option<NonZeroU32>,
}
/// 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<String>, label: &str, path: &Path) {
}
}
fn push_configured_updater(details: &mut Vec<String>, 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::<ConfiguredSettings>(&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::<Vec<_>>()
.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");
}
}
}
}
@@ -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)