Files
codex/codex-rs/app-server-daemon/src/settings_tests.rs
T
Eric Traut 1537497e52 Allow compatible feature overrides when starting the shared daemon (#46529)
## Why

Feature overrides previously forced embedded mode even when they could work with the shared daemon. A running daemon can also have different feature settings from the current invocation, including when the invocation uses defaults.

## What changed

- Allow selected Boolean feature overrides and `suppress_unstable_features_warning` in daemon mode. Keep explicit overrides that disable shared services in embedded mode.
- Pass shared-service feature overrides to newly launched daemons and persist them for restarts and updates without changing a running daemon's settings.
- Check daemon feature settings with `experimentalFeature/list` before attaching. Warn and fall back to embedded mode on mismatches, failed checks, or an incompatible code-mode host fallback policy. Skip this check for the agents overview.
- Resolve worktree configuration before daemon selection and allow worktree sessions to auto-start the daemon.
- Forward `suppress_unstable_features_warning` in thread configuration overrides.

## Testing

Add coverage for override eligibility and precedence, launch-feature persistence and reuse, and TUI fallback warnings for feature and host-policy mismatches. Extend worktree integration coverage to exercise daemon auto-start and warning suppression.

GitOrigin-RevId: 68c316f74843b1dfd8fa39bdb48bd1f649fee57f
2026-09-19 00:14:40 +00:00

156 lines
4.8 KiB
Rust

use pretty_assertions::assert_eq;
use tempfile::TempDir;
use super::DaemonSettings;
use super::MAX_SHUTDOWN_GRACE_SECONDS;
#[tokio::test]
async fn remote_control_save_preserves_updater_settings() {
let temp = TempDir::new().expect("temp dir");
let path = temp.path().join("settings.json");
tokio::fs::write(&path, r#"{"remoteControlEnabled":true}"#)
.await
.expect("write legacy settings");
let settings = DaemonSettings::load(&path).await.expect("load settings");
assert_eq!(
settings,
DaemonSettings {
remote_control_enabled: true,
..DaemonSettings::default()
}
);
tokio::fs::write(
&path,
r#"{"remoteControlEnabled":true,"shutdownGraceSeconds":25,"updater":{"autoUpdateEnabled":false,"updateIntervalMinutes":17},"featureOverrides":{"api_key_model_discovery":true,"code_mode_host":false},"futureSetting":42}"#,
)
.await
.expect("write settings");
let updated = DaemonSettings {
remote_control_enabled: false,
..DaemonSettings::load(&path)
.await
.expect("load updater settings")
};
updated.save(&path).await.expect("save remote control");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(
&tokio::fs::read(&path).await.expect("read settings")
)
.expect("parse settings"),
serde_json::json!({
"remoteControlEnabled": false,
"shutdownGraceSeconds": 25,
"updater": {"autoUpdateEnabled": false, "updateIntervalMinutes": 17},
"featureOverrides": {"api_key_model_discovery": true, "code_mode_host": false},
"futureSetting": 42,
})
);
}
#[tokio::test]
async fn shutdown_grace_accepts_zero_through_five_minutes() {
let temp = TempDir::new().expect("temp dir");
let path = temp.path().join("settings.json");
assert_eq!(
DaemonSettings::load(&path).await.expect("missing settings"),
DaemonSettings::default()
);
assert_eq!(
DaemonSettings::load_for_stop(&path)
.await
.shutdown_grace_seconds,
60
);
for seconds in [
0,
MAX_SHUTDOWN_GRACE_SECONDS,
MAX_SHUTDOWN_GRACE_SECONDS + 1,
] {
tokio::fs::write(
&path,
serde_json::to_vec(&serde_json::json!({"shutdownGraceSeconds": seconds}))
.expect("serialize settings"),
)
.await
.expect("write settings");
let expected = (seconds <= MAX_SHUTDOWN_GRACE_SECONDS).then_some(seconds);
assert_eq!(
DaemonSettings::load(&path)
.await
.ok()
.map(|settings| settings.shutdown_grace_seconds),
expected
);
assert_eq!(
DaemonSettings::load_for_stop(&path)
.await
.shutdown_grace_seconds,
expected.unwrap_or(60)
);
}
tokio::fs::write(&path, r#"{"shutdownGraceSeconds":"unlimited"}"#)
.await
.expect("write invalid settings");
assert!(DaemonSettings::load(&path).await.is_err());
assert_eq!(
DaemonSettings::load_for_stop(&path)
.await
.shutdown_grace_seconds,
60
);
}
#[tokio::test]
async fn update_interval_accepts_long_values_and_rejects_zero() {
let temp = TempDir::new().expect("temp dir");
let path = temp.path().join("settings.json");
for minutes in [u32::MAX, 0] {
tokio::fs::write(
&path,
serde_json::to_vec(&serde_json::json!({
"updater": {"updateIntervalMinutes": minutes},
}))
.expect("serialize invalid settings"),
)
.await
.expect("write invalid settings");
let loaded = DaemonSettings::load(&path).await;
if minutes == 0 {
assert!(loaded.is_err());
} else {
assert_eq!(
loaded.expect("load long interval").update_interval_minutes,
minutes
);
}
}
}
#[tokio::test]
async fn telemetry_distinguishes_presence_from_default_values() -> anyhow::Result<()> {
let home = TempDir::new()?;
let dir = home.path().join("app-server-daemon");
tokio::fs::create_dir(&dir).await?;
let path = dir.join("settings.json");
for (contents, presence) in [
("{}", "default"),
(
r#"{"updater":{"autoUpdateEnabled":true,"updateIntervalMinutes":60},"shutdownGraceSeconds":60}"#,
"configured",
),
] {
tokio::fs::write(&path, contents).await?;
assert_eq!(
crate::telemetry::settings_tags(home.path())
.await
.map(|(_, value)| value),
["enabled", presence, presence, presence]
);
}
Ok(())
}