config: remove legacy profile write paths (#24055)

## Why

[#23883](https://github.com/openai/codex/pull/23883) moved the
user-facing `--profile` flag onto profile v2 and
[#23886](https://github.com/openai/codex/pull/23886) removed CLI
forwarding for the legacy profile-v1 path. Core and TUI config
persistence still carried `active_profile` and
`ConfigEditsBuilder::with_profile`, which let later writes continue
targeting legacy `[profiles.<name>]` tables after profile selection
moved to profile-v2 config files.

## What

- Remove legacy profile routing from
[`ConfigEditsBuilder`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/core/src/config/edit.rs#L1064-L1294),
so core config edits no longer carry `with_profile` or infer
`[profiles.*]` write targets from a `profile` key.
- Drop `active_profile` plumbing from runtime `Config`, TUI
startup/state, app-server config override forwarding, and Windows
sandbox setup persistence.
- Make app-server-backed TUI config edits use unscoped model,
service-tier, feature, Auto-review, plan-mode, and Windows sandbox paths
through
[`tui/src/config_update.rs`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/tui/src/config_update.rs#L43-L112).
- Update config edit coverage so legacy `profile` state stays untouched
by direct model writes, and remove tests whose only contract was the
deleted profile-scoped persistence path.

## Testing

- Not run locally.
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-22 12:50:42 +02:00
1 parent fd72e99384
commit 2c6605ab35
18 files changed
+107 -974

No files matched your search

-206
View File
@@ -4798,7 +4798,6 @@ model = "gpt-project-local"
.build()
.await?;
assert_eq!(config.active_profile, None);
assert_eq!(config.model, None);
assert!(
config.startup_warnings.iter().any(|warning| {
@@ -5032,7 +5031,6 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5063,7 +5061,6 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
let empty = BTreeMap::new();
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(empty.clone())],
)?;
let loaded = load_global_mcp_servers(codex_home.path()).await?;
@@ -5379,7 +5376,6 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5456,7 +5452,6 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5518,7 +5513,6 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5570,7 +5564,6 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5625,7 +5618,6 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5695,7 +5687,6 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
)]);
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5779,7 +5770,6 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let serialized_with_optional = std::fs::read_to_string(&config_path)?;
@@ -5814,7 +5804,6 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
);
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -5913,7 +5902,6 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -6001,7 +5989,6 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -6052,7 +6039,6 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -6103,7 +6089,6 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -6160,7 +6145,6 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
@@ -6283,196 +6267,6 @@ model = "gpt-4.1"
Ok(())
}
#[tokio::test]
async fn set_model_updates_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_model(Some("gpt-5.4"), Some(ReasoningEffort::Medium))
.apply()
.await?;
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
let parsed: ConfigToml = toml::from_str(&serialized)?;
let profile = parsed
.profiles
.get("dev")
.expect("profile should be created");
assert_eq!(profile.model.as_deref(), Some("gpt-5.4"));
assert_eq!(
profile.model_reasoning_effort,
Some(ReasoningEffort::Medium)
);
Ok(())
}
#[tokio::test]
async fn set_model_updates_existing_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
tokio::fs::write(
&config_path,
r#"
[profiles.dev]
model = "gpt-4"
model_reasoning_effort = "medium"
[profiles.prod]
model = "gpt-5.4"
"#,
)
.await?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_model(Some("o4-high"), Some(ReasoningEffort::Medium))
.apply()
.await?;
let serialized = tokio::fs::read_to_string(config_path).await?;
let parsed: ConfigToml = toml::from_str(&serialized)?;
let dev_profile = parsed
.profiles
.get("dev")
.expect("dev profile should survive updates");
assert_eq!(dev_profile.model.as_deref(), Some("o4-high"));
assert_eq!(
dev_profile.model_reasoning_effort,
Some(ReasoningEffort::Medium)
);
assert_eq!(
parsed
.profiles
.get("prod")
.and_then(|profile| profile.model.as_deref()),
Some("gpt-5.4"),
);
Ok(())
}
#[tokio::test]
async fn set_feature_enabled_updates_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_feature_enabled("guardian_approval", /*enabled*/ true)
.apply()
.await?;
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
let parsed: ConfigToml = toml::from_str(&serialized)?;
let profile = parsed
.profiles
.get("dev")
.expect("profile should be created");
assert_eq!(
profile
.features
.as_ref()
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(true),
);
assert_eq!(
parsed
.features
.as_ref()
.and_then(|features| features.entries().get("guardian_approval").copied()),
None,
);
Ok(())
}
#[tokio::test]
async fn set_feature_enabled_persists_feature_disable_in_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_feature_enabled("guardian_approval", /*enabled*/ true)
.apply()
.await?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_feature_enabled("guardian_approval", /*enabled*/ false)
.apply()
.await?;
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
let parsed: ConfigToml = toml::from_str(&serialized)?;
let profile = parsed
.profiles
.get("dev")
.expect("profile should be created");
assert_eq!(
profile
.features
.as_ref()
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(false),
);
assert_eq!(
parsed
.features
.as_ref()
.and_then(|features| features.entries().get("guardian_approval").copied()),
None,
);
Ok(())
}
#[tokio::test]
async fn set_feature_enabled_profile_disable_overrides_root_enable() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
ConfigEditsBuilder::new(codex_home.path())
.set_feature_enabled("guardian_approval", /*enabled*/ true)
.apply()
.await?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_feature_enabled("guardian_approval", /*enabled*/ false)
.apply()
.await?;
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
let parsed: ConfigToml = toml::from_str(&serialized)?;
let profile = parsed
.profiles
.get("dev")
.expect("profile should be created");
assert_eq!(
parsed
.features
.as_ref()
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(true),
);
assert_eq!(
profile
.features
.as_ref()
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(false),
);
Ok(())
}
struct PrecedenceTestFixture {
cwd: TempDir,
codex_home: TempDir,
+36 -135
View File
@@ -513,13 +513,6 @@ mod document_helpers {
struct ConfigDocument {
doc: DocumentMut,
profile: Option<String>,
}
#[derive(Copy, Clone)]
enum Scope {
Global,
Profile,
}
#[derive(Copy, Clone)]
@@ -529,25 +522,25 @@ enum TraversalMode {
}
impl ConfigDocument {
fn new(doc: DocumentMut, profile: Option<String>) -> Self {
Self { doc, profile }
fn new(doc: DocumentMut) -> Self {
Self { doc }
}
fn apply(&mut self, edit: &ConfigEdit) -> anyhow::Result<bool> {
match edit {
ConfigEdit::SetModel { model, effort } => Ok({
let mut mutated = false;
mutated |= self.write_profile_value(
mutated |= self.write_optional_value(
&["model"],
model.as_ref().map(|model_value| value(model_value.clone())),
);
mutated |= self.write_profile_value(
mutated |= self.write_optional_value(
&["model_reasoning_effort"],
effort.map(|effort| value(effort.to_string())),
);
mutated
}),
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_optional_value(
&["service_tier"],
service_tier.as_ref().map(|service_tier| {
// Keep the legacy config spelling stable. Runtime values use
@@ -560,35 +553,30 @@ impl ConfigDocument {
value(config_value)
}),
)),
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value(
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_optional_value(
&["personality"],
personality.map(|personality| value(personality.to_string())),
)),
ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, "hide_full_access_warning"],
value(*acknowledged),
)),
ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged) => Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, "hide_world_writable_warning"],
value(*acknowledged),
)),
ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"],
value(*acknowledged),
)),
ConfigEdit::SetNoticeHideModelMigrationPrompt(migration_config, acknowledged) => {
Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, migration_config.as_str()],
value(*acknowledged),
))
}
ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(acknowledged) => Ok(self
.write_value(
Scope::Global,
&[
NOTICE_TABLE_KEY,
"external_config_migration_prompts",
@@ -598,7 +586,6 @@ impl ConfigDocument {
)),
ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(timestamp) => {
Ok(self.write_value(
Scope::Global,
&[
NOTICE_TABLE_KEY,
"external_config_migration_prompts",
@@ -611,7 +598,6 @@ impl ConfigDocument {
project,
acknowledged,
) => Ok(self.write_value(
Scope::Global,
&[
NOTICE_TABLE_KEY,
"external_config_migration_prompts",
@@ -624,7 +610,6 @@ impl ConfigDocument {
project,
timestamp,
) => Ok(self.write_value(
Scope::Global,
&[
NOTICE_TABLE_KEY,
"external_config_migration_prompts",
@@ -634,7 +619,6 @@ impl ConfigDocument {
value(*timestamp),
)),
ConfigEdit::RecordModelMigrationSeen { from, to } => Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, "model_migrations", from.as_str()],
value(to.clone()),
)),
@@ -663,20 +647,26 @@ impl ConfigDocument {
}
}
fn write_profile_value(&mut self, segments: &[&str], value: Option<TomlItem>) -> bool {
fn write_optional_value(&mut self, segments: &[&str], value: Option<TomlItem>) -> bool {
match value {
Some(item) => self.write_value(Scope::Profile, segments, item),
None => self.clear(Scope::Profile, segments),
Some(item) => self.write_value(segments, item),
None => self.clear(segments),
}
}
fn write_value(&mut self, scope: Scope, segments: &[&str], value: TomlItem) -> bool {
let resolved = self.scoped_segments(scope, segments);
fn write_value(&mut self, segments: &[&str], value: TomlItem) -> bool {
let resolved = segments
.iter()
.map(|segment| (*segment).to_string())
.collect::<Vec<_>>();
self.insert(&resolved, value)
}
fn clear(&mut self, scope: Scope, segments: &[&str]) -> bool {
let resolved = self.scoped_segments(scope, segments);
fn clear(&mut self, segments: &[&str]) -> bool {
let resolved = segments
.iter()
.map(|segment| (*segment).to_string())
.collect::<Vec<_>>();
self.remove(&resolved)
}
@@ -709,7 +699,6 @@ impl ConfigDocument {
.filter(|disabled_tool| seen.insert(disabled_tool.clone()))
.collect::<Vec<_>>();
self.write_value(
Scope::Global,
&["tool_suggest", "disabled_tools"],
document_helpers::tool_suggest_disabled_tools_value(&disabled_tools),
)
@@ -721,7 +710,7 @@ impl ConfigDocument {
fn replace_mcp_servers(&mut self, servers: &BTreeMap<String, McpServerConfig>) -> bool {
if servers.is_empty() {
return self.clear(Scope::Global, &["mcp_servers"]);
return self.clear(&["mcp_servers"]);
}
let root = self.doc.as_table_mut();
@@ -883,26 +872,6 @@ impl ConfigDocument {
mutated
}
fn scoped_segments(&self, scope: Scope, segments: &[&str]) -> Vec<String> {
let resolved: Vec<String> = segments
.iter()
.map(|segment| (*segment).to_string())
.collect();
if matches!(scope, Scope::Profile)
&& resolved.first().is_none_or(|segment| segment != "profiles")
&& let Some(profile) = self.profile.as_deref()
{
let mut scoped = Vec::with_capacity(resolved.len() + 2);
scoped.push("profiles".to_string());
scoped.push(profile.to_string());
scoped.extend(resolved);
return scoped;
}
resolved
}
fn insert(&mut self, segments: &[String], value: TomlItem) -> bool {
let Some((last, parents)) = segments.split_last() else {
return false;
@@ -1030,18 +999,13 @@ fn write_skill_config_selector(table: &mut TomlTable, selector: &SkillConfigSele
}
/// Persist edits using a blocking strategy.
pub fn apply_blocking(
codex_home: &Path,
profile: Option<&str>,
edits: &[ConfigEdit],
) -> anyhow::Result<()> {
pub fn apply_blocking(codex_home: &Path, edits: &[ConfigEdit]) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
apply_blocking_to_resolved_file(&config_path, profile, edits)
apply_blocking_to_resolved_file(&config_path, edits)
}
fn apply_blocking_to_resolved_file(
resolved_config_file: &Path,
legacy_profile: Option<&str>,
edits: &[ConfigEdit],
) -> anyhow::Result<()> {
if edits.is_empty() {
@@ -1064,13 +1028,7 @@ fn apply_blocking_to_resolved_file(
serialized.parse::<DocumentMut>()?
};
let profile = legacy_profile.map(ToOwned::to_owned).or_else(|| {
doc.get("profile")
.and_then(|item| item.as_str())
.map(ToOwned::to_owned)
});
let mut document = ConfigDocument::new(doc, profile);
let mut document = ConfigDocument::new(doc);
let mut mutated = false;
for edit in edits {
@@ -1093,29 +1051,18 @@ fn apply_blocking_to_resolved_file(
/// Persist edits asynchronously by offloading the blocking writer.
///
/// `profile` selects a legacy `[profiles.<name>]` section inside
/// `$CODEX_HOME/config.toml`; profile-v2 callers should resolve their target
/// file before constructing a [ConfigEditsBuilder].
pub async fn apply(
codex_home: &Path,
profile: Option<&str>,
edits: Vec<ConfigEdit>,
) -> anyhow::Result<()> {
pub async fn apply(codex_home: &Path, edits: Vec<ConfigEdit>) -> anyhow::Result<()> {
let codex_home = codex_home.to_path_buf();
let config_path = codex_home.join(CONFIG_TOML_FILE);
let profile = profile.map(ToOwned::to_owned);
task::spawn_blocking(move || {
apply_blocking_to_resolved_file(&config_path, profile.as_deref(), &edits)
})
.await
.context("config persistence task panicked")?
task::spawn_blocking(move || apply_blocking_to_resolved_file(&config_path, &edits))
.await
.context("config persistence task panicked")?
}
/// Fluent builder to batch config edits and apply them atomically.
#[derive(Default)]
pub struct ConfigEditsBuilder {
config_path: PathBuf,
profile: Option<String>,
edits: Vec<ConfigEdit>,
}
@@ -1136,16 +1083,10 @@ impl ConfigEditsBuilder {
pub fn for_config_path(config_path: &Path) -> Self {
Self {
config_path: config_path.to_path_buf(),
profile: None,
edits: Vec::new(),
}
}
pub fn with_profile(mut self, profile: Option<&str>) -> Self {
self.profile = profile.map(ToOwned::to_owned);
self
}
pub fn set_model(mut self, model: Option<&str>, effort: Option<ReasoningEffort>) -> Self {
self.edits.push(ConfigEdit::SetModel {
model: model.map(ToOwned::to_owned),
@@ -1248,27 +1189,16 @@ impl ConfigEditsBuilder {
/// Enable or disable a feature flag by key under the `[features]` table.
///
/// Disabling a default-false feature clears the root-scoped key instead of
/// Disabling a default-false feature clears the key instead of
/// persisting `false`, so the config does not pin the feature once it
/// graduates to globally enabled. Profile-scoped disables still persist
/// `false` so they can override an inherited root enable.
/// graduates to globally enabled.
pub fn set_feature_enabled(mut self, key: &str, enabled: bool) -> Self {
let profile_scoped = self.profile.is_some();
let segments = if let Some(profile) = self.profile.as_ref() {
vec![
"profiles".to_string(),
profile.clone(),
"features".to_string(),
key.to_string(),
]
} else {
vec!["features".to_string(), key.to_string()]
};
let segments = vec!["features".to_string(), key.to_string()];
let is_default_false_feature = FEATURES
.iter()
.find(|spec| spec.key == key)
.is_some_and(|spec| !spec.default_enabled);
if enabled || profile_scoped || !is_default_false_feature {
if enabled || !is_default_false_feature {
self.edits.push(ConfigEdit::SetPath {
segments,
value: value(enabled),
@@ -1280,18 +1210,8 @@ impl ConfigEditsBuilder {
}
pub fn set_windows_sandbox_mode(mut self, mode: &str) -> Self {
let segments = if let Some(profile) = self.profile.as_ref() {
vec![
"profiles".to_string(),
profile.clone(),
"windows".to_string(),
"sandbox".to_string(),
]
} else {
vec!["windows".to_string(), "sandbox".to_string()]
};
self.edits.push(ConfigEdit::SetPath {
segments,
segments: vec!["windows".to_string(), "sandbox".to_string()],
value: value(mode),
});
self
@@ -1339,34 +1259,15 @@ impl ConfigEditsBuilder {
"elevated_windows_sandbox",
"enable_experimental_windows_sandbox",
] {
let mut segments = vec!["features".to_string(), key.to_string()];
if let Some(profile) = self.profile.as_ref() {
segments = vec![
"profiles".to_string(),
profile.clone(),
"features".to_string(),
key.to_string(),
];
}
let segments = vec!["features".to_string(), key.to_string()];
self.edits.push(ConfigEdit::ClearPath { segments });
}
self
}
pub fn set_session_picker_view(mut self, mode: SessionPickerViewMode) -> Self {
let segments = if let Some(profile) = self.profile.as_ref() {
vec![
"profiles".to_string(),
profile.clone(),
"tui".to_string(),
"session_picker_view".to_string(),
]
} else {
vec!["tui".to_string(), "session_picker_view".to_string()]
};
self.edits.push(ConfigEdit::SetPath {
segments,
segments: vec!["tui".to_string(), "session_picker_view".to_string()],
value: value(mode.to_string()),
});
self
@@ -1382,13 +1283,13 @@ impl ConfigEditsBuilder {
/// Apply edits on a blocking thread.
pub fn apply_blocking(self) -> anyhow::Result<()> {
apply_blocking_to_resolved_file(&self.config_path, self.profile.as_deref(), &self.edits)
apply_blocking_to_resolved_file(&self.config_path, &self.edits)
}
/// Apply edits asynchronously via a blocking offload.
pub async fn apply(self) -> anyhow::Result<()> {
task::spawn_blocking(move || {
apply_blocking_to_resolved_file(&self.config_path, self.profile.as_deref(), &self.edits)
apply_blocking_to_resolved_file(&self.config_path, &self.edits)
})
.await
.context("config persistence task panicked")?
+19 -106
View File
@@ -20,7 +20,6 @@ fn blocking_set_model_top_level() {
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetModel {
model: Some("gpt-5.4".to_string()),
effort: Some(ReasoningEffort::High),
@@ -111,24 +110,6 @@ session_picker_view = "dense"
assert_eq!(contents, expected);
}
#[test]
fn session_picker_view_builder_respects_active_profile() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
ConfigEditsBuilder::new(codex_home)
.with_profile(Some("work"))
.set_session_picker_view(SessionPickerViewMode::Dense)
.apply_blocking()
.expect("persist");
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[profiles.work.tui]
session_picker_view = "dense"
"#;
assert_eq!(contents, expected);
}
#[test]
fn keymap_binding_edit_writes_root_action_binding() {
let tmp = tempdir().expect("tmpdir");
@@ -380,7 +361,7 @@ enabled = false
}
#[test]
fn blocking_set_model_preserves_inline_table_contents() {
fn blocking_set_model_ignores_inline_legacy_profile_contents() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
@@ -396,7 +377,6 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetModel {
model: Some("o4-mini".to_string()),
effort: None,
@@ -407,7 +387,12 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let value: TomlValue = toml::from_str(&raw).expect("parse config");
// Ensure sandbox_mode is preserved under profiles.fast and model updated.
assert_eq!(
value.get("model").and_then(TomlValue::as_str),
Some("o4-mini")
);
// Legacy profile values stay untouched when root settings are updated.
let profiles_tbl = value
.get("profiles")
.and_then(|v| v.as_table())
@@ -422,7 +407,7 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
);
assert_eq!(
fast_tbl.get("model").and_then(|v| v.as_str()),
Some("o4-mini")
Some("gpt-4o")
);
}
@@ -441,7 +426,6 @@ fn blocking_set_model_writes_through_symlink_chain() {
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetModel {
model: Some("gpt-5.4".to_string()),
effort: Some(ReasoningEffort::High),
@@ -474,7 +458,6 @@ fn blocking_set_model_replaces_symlink_on_cycle() {
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetModel {
model: Some("gpt-5.4".to_string()),
effort: None,
@@ -513,7 +496,6 @@ network_access = false
apply_blocking(
codex_home,
/*profile*/ None,
&[
ConfigEdit::SetPath {
segments: vec![
@@ -553,7 +535,7 @@ network_access = true
}
#[test]
fn blocking_clear_model_removes_inline_table_entry() {
fn blocking_clear_model_does_not_follow_legacy_active_profile() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
@@ -568,7 +550,6 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetModel {
model: None,
effort: Some(ReasoningEffort::High),
@@ -579,15 +560,14 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"profile = "fast"
[profiles.fast]
sandbox_mode = "strict"
profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
model_reasoning_effort = "high"
"#;
assert_eq!(contents, expected);
}
#[test]
fn blocking_set_model_scopes_to_active_profile() {
fn blocking_set_model_does_not_follow_legacy_active_profile() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
std::fs::write(
@@ -602,7 +582,6 @@ model_reasoning_effort = "low"
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetModel {
model: Some("o5-preview".to_string()),
effort: Some(ReasoningEffort::Minimal),
@@ -612,39 +591,11 @@ model_reasoning_effort = "low"
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"profile = "team"
model = "o5-preview"
model_reasoning_effort = "minimal"
[profiles.team]
model_reasoning_effort = "minimal"
model = "o5-preview"
"#;
assert_eq!(contents, expected);
}
#[test]
fn blocking_set_model_with_explicit_profile() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
std::fs::write(
codex_home.join(CONFIG_TOML_FILE),
r#"[profiles."team a"]
model = "gpt-5.4"
"#,
)
.expect("seed");
apply_blocking(
codex_home,
Some("team a"),
&[ConfigEdit::SetModel {
model: Some("o4-mini".to_string()),
effort: None,
}],
)
.expect("persist");
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[profiles."team a"]
model = "o4-mini"
model_reasoning_effort = "low"
"#;
assert_eq!(contents, expected);
}
@@ -666,7 +617,6 @@ existing = "value"
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetNoticeHideFullAccessWarning(true)],
)
.expect("persist");
@@ -696,7 +646,6 @@ existing = "value"
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetNoticeHideRateLimitModelNudge(true)],
)
.expect("persist");
@@ -722,7 +671,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetNoticeHideModelMigrationPrompt(
"hide_gpt5_1_migration_prompt".to_string(),
true,
@@ -751,7 +699,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetNoticeHideModelMigrationPrompt(
"hide_gpt-5.1-codex-max_migration_prompt".to_string(),
true,
@@ -780,7 +727,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::RecordModelMigrationSeen {
from: "gpt-5.2".to_string(),
to: "gpt-5.4".to_string(),
@@ -811,7 +757,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(
true,
)],
@@ -841,7 +786,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[
ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject(
"/Users/alexsong/code/skills".to_string(),
@@ -874,7 +818,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(1_760_000_000)],
)
.expect("persist");
@@ -902,7 +845,6 @@ existing = "value"
.expect("seed");
apply_blocking(
codex_home,
/*profile*/ None,
&[
ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(
"/Users/alexsong/code/skills".to_string(),
@@ -996,7 +938,6 @@ fn blocking_replace_mcp_servers_round_trips() {
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)
.expect("persist");
@@ -1069,12 +1010,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
},
);
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers)],
)
.expect("persist");
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = "\
@@ -1129,12 +1065,7 @@ foo = { command = "cmd" }
},
);
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers)],
)
.expect("persist");
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[mcp_servers]
@@ -1184,12 +1115,7 @@ foo = { command = "cmd" } # keep me
},
);
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers)],
)
.expect("persist");
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[mcp_servers]
@@ -1238,12 +1164,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
},
);
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers)],
)
.expect("persist");
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[mcp_servers]
@@ -1293,12 +1214,7 @@ foo = { command = "cmd" }
},
);
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers)],
)
.expect("persist");
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[mcp_servers]
@@ -1315,7 +1231,6 @@ fn blocking_clear_path_noop_when_missing() {
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ClearPath {
segments: vec!["missing".to_string()],
}],
@@ -1336,7 +1251,6 @@ fn blocking_set_path_updates_notifications() {
let item = value(false);
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::SetPath {
segments: vec!["tui".to_string(), "notifications".to_string()],
value: item,
@@ -1518,7 +1432,6 @@ fn replace_mcp_servers_blocking_clears_table_when_empty() {
apply_blocking(
codex_home,
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(BTreeMap::new())],
)
.expect("persist");
-4
View File
@@ -958,9 +958,6 @@ pub struct Config {
/// When `true`, suppress warnings about unstable (under development) features.
pub suppress_unstable_features_warning: bool,
/// The active profile name used to derive this `Config` (if any).
pub active_profile: Option<String>,
/// The currently active project config, resolved by checking if cwd:
/// is (1) part of a git repo, (2) a git worktree, or (3) just using the cwd
pub active_project: ProjectConfig,
@@ -3499,7 +3496,6 @@ impl Config {
suppress_unstable_features_warning: cfg
.suppress_unstable_features_warning
.unwrap_or(false),
active_profile: None,
active_project,
notices,
check_for_update_on_startup,