Warn about ignored configuration settings (#44691)

## Why

Unrecognized settings can be silently ignored, leaving typos or deprecated configuration unnoticed.

## What changed

- Collect warnings for unrecognized fields in effective configuration and requirements layers, including unknown feature keys in configuration.
- Report sources and field names without configuration values, show up to three entries, and provide migration hints for `network_proxy`, `allowed_permissions`, and `include_view_image_tool`.
- Emit project-specific warnings when starting a thread and suppress repeated startup warnings in the app server and TUI.

## Testing

Add coverage for merged configuration, disabled layers, startup and project warning delivery, omission of configuration values, and warning deduplication.

GitOrigin-RevId: 464cef87882502b19865ae10b50b6f82defb7296
This commit is contained in:
andrewgu-oai authored and copyberry committed 2026-09-11 00:55:51 +00:00
1 parent 9e22e74e8d
commit e53c444964
11 files changed
+305 -44

No files matched your search

@@ -1,7 +1,76 @@
use std::process::Command;
//! Config errors and startup/project warnings through the public API.
use anyhow::Result;
use app_test_support::TestAppServer;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::ThreadStartParams;
use codex_core::config::set_project_trust_level;
use codex_protocol::config_types::TrustLevel;
use pretty_assertions::assert_ne;
use std::process::Command;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
#[tokio::test]
async fn ignored_config_fields_emit_startup_and_project_warnings() -> Result<()> {
let home = TempDir::new()?;
std::fs::write(
home.path().join("config.toml"),
"network_proxy = { nested = 'private_value' }",
)?;
std::fs::write(
home.path().join("requirements.toml"),
"allowed_permissions = [':read-only']",
)?;
let read_timeout = Duration::from_secs(30);
let mut server = TestAppServer::builder()
.with_codex_home(home.path())
.build_initialized_with_timeout(read_timeout)
.await?;
let startup_warning: ConfigWarningNotification =
timeout(read_timeout, server.read_notification("configWarning")).await??;
let summary = &startup_warning.summary;
assert!(summary.contains("`network_proxy` is ignored."));
assert!(summary.contains("`allowed_permissions` is ignored."));
assert!(!summary.contains("private_value"));
server.start_thread(ThreadStartParams::default()).await?;
// Config warnings are sent before the thread/start response. Other startup
// warnings (e.g. missing bubblewrap on Linux) may still be buffered here.
while server
.pending_notification_methods()
.iter()
.any(|method| method == "configWarning")
{
let warning: ConfigWarningNotification =
timeout(read_timeout, server.read_notification("configWarning")).await??;
assert_ne!(
warning, startup_warning,
"thread/start repeated the startup warning"
);
}
let project = TempDir::new()?;
std::fs::create_dir(project.path().join(".git"))?;
std::fs::create_dir(project.path().join(".codex"))?;
std::fs::write(
project.path().join(".codex/config.toml"),
"project_setting = 'private_value'",
)?;
set_project_trust_level(home.path(), project.path(), TrustLevel::Trusted)?;
server
.start_thread(ThreadStartParams {
cwd: Some(project.path().display().to_string()),
..Default::default()
})
.await?;
let warning: ConfigWarningNotification =
timeout(read_timeout, server.read_notification("configWarning")).await??;
assert!(warning.summary.contains("`project_setting` is ignored."));
assert!(!warning.summary.contains("private_value"));
Ok(())
}
#[test]
fn strict_config_rejects_unknown_config_fields_for_standalone_app_server() -> Result<()> {