mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: add layered --profile-v2 config files (#17141)
## Why `--profile-v2 <name>` gives launchers and runtime entry points a named profile config without making each profile duplicate the base user config. The base `$CODEX_HOME/config.toml` still loads first, then `$CODEX_HOME/<name>.config.toml` layers above it and becomes the active writable user config for that session. That keeps shared defaults, plugin/MCP setup, and managed/user constraints in one place while letting a named profile override only the pieces that need to differ. ## What Changed - Added the shared `--profile-v2 <name>` runtime option with validated plain names, now represented by `ProfileV2Name`. - Extended config layer state so the base user config and selected profile config are both `User` layers; APIs expose the active user layer and merged effective user config. - Threaded profile selection through runtime entry points: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, and `codex debug prompt-input`. - Made user-facing config writes go to the selected profile file when active, including TUI/settings persistence, app-server config writes, and MCP/app tool approval persistence. - Made plugin, marketplace, MCP, hooks, and config reload paths read from the merged user config so base and profile layers both participate. - Updated app-server config layer schemas to mark profile-backed user layers. ## Limits `--profile-v2` is still rejected for config-management subcommands such as feature, MCP, and marketplace edits. Those paths remain tied to the base `config.toml` until they have explicit profile-selection semantics. Some adjacent background writes may still update base or global state rather than the selected profile: - marketplace auto-upgrade metadata - automatic MCP dependency installs from skills - remote plugin sync or uninstall config edits - personality migration marker/default writes ## Verification Added targeted coverage for profile name validation, layer ordering/merging, selected-profile writes, app-server config writes, session hot reload, plugin config merging, hooks/config fixture updates, and MCP/app approval persistence. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -229,7 +229,7 @@ where
|
||||
fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Option<PathBuf> {
|
||||
match &layer.name {
|
||||
ConfigLayerSource::System { file } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::User { file } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::User { file, .. } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
Some(dot_codex_folder.as_path().join(config_toml_file))
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ pub use cloud_requirements::CloudRequirementsLoadError;
|
||||
pub use cloud_requirements::CloudRequirementsLoadErrorCode;
|
||||
pub use cloud_requirements::CloudRequirementsLoader;
|
||||
pub use codex_app_server_protocol::ConfigLayerSource;
|
||||
pub use codex_protocol::config_types::ProfileV2Name;
|
||||
pub use codex_protocol::config_types::ProfileV2NameParseError;
|
||||
pub use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
pub use config_requirements::AppRequirementToml;
|
||||
pub use config_requirements::AppToolRequirementToml;
|
||||
|
||||
@@ -4,6 +4,7 @@ mod macos;
|
||||
|
||||
use self::layer_io::LoadedConfigLayers;
|
||||
use crate::CONFIG_TOML_FILE;
|
||||
use crate::ProfileV2Name;
|
||||
use crate::cloud_requirements::CloudRequirementsLoader;
|
||||
use crate::config_requirements::ConfigRequirementsToml;
|
||||
use crate::config_requirements::ConfigRequirementsWithSources;
|
||||
@@ -89,6 +90,7 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
|
||||
/// - system `/etc/codex/config.toml` (Unix) or
|
||||
/// `%ProgramData%\OpenAI\Codex\config.toml` (Windows)
|
||||
/// - user `${CODEX_HOME}/config.toml`
|
||||
/// - profile `${CODEX_HOME}/<name>.config.toml`, when selected
|
||||
/// - cwd `${PWD}/config.toml` (loaded but disabled when the directory is untrusted)
|
||||
/// - tree parent directories up to root looking for `./.codex/config.toml` (loaded but disabled when untrusted)
|
||||
/// - repo `$(git rev-parse --show-toplevel)/.codex/config.toml` (loaded but disabled when untrusted)
|
||||
@@ -116,6 +118,7 @@ pub async fn load_config_layers_state(
|
||||
loader_overrides: overrides,
|
||||
strict_config,
|
||||
} = options.into();
|
||||
let active_user_profile = overrides.user_config_profile.clone();
|
||||
let ignore_managed_requirements = overrides.ignore_managed_requirements;
|
||||
let ignore_user_config = overrides.ignore_user_config;
|
||||
let ignore_user_and_project_exec_policy_rules =
|
||||
@@ -205,29 +208,34 @@ pub async fn load_config_layers_state(
|
||||
.await?;
|
||||
layers.push(system_layer);
|
||||
|
||||
// Add a layer for $CODEX_HOME/config.toml so folder-derived resources such
|
||||
// as rules/ can still be discovered. When user config is ignored, preserve
|
||||
// the layer metadata without reading config.toml.
|
||||
let user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
|
||||
let user_layer = if ignore_user_config {
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
},
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
// Add the base user config layer. When profile-v2 is selected, add the
|
||||
// profile config as a second user layer on top so the profile only needs to
|
||||
// contain overrides.
|
||||
let base_user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
|
||||
layers.push(
|
||||
load_user_config_layer(
|
||||
fs,
|
||||
&base_user_file,
|
||||
/*profile*/ None,
|
||||
ignore_user_config,
|
||||
strict_config,
|
||||
)
|
||||
} else {
|
||||
load_config_toml_for_required_layer(fs, &user_file, strict_config, |config_toml| {
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
},
|
||||
config_toml,
|
||||
.await?,
|
||||
);
|
||||
|
||||
let active_user_file = overrides.user_config_path(codex_home)?;
|
||||
if active_user_file != base_user_file {
|
||||
layers.push(
|
||||
load_user_config_layer(
|
||||
fs,
|
||||
&active_user_file,
|
||||
active_user_profile.as_ref(),
|
||||
ignore_user_config,
|
||||
strict_config,
|
||||
)
|
||||
})
|
||||
.await?
|
||||
};
|
||||
layers.push(user_layer);
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let mut startup_warnings = None;
|
||||
if let Some(cwd) = cwd {
|
||||
@@ -258,7 +266,7 @@ pub async fn load_config_layers_state(
|
||||
&cwd,
|
||||
&project_root_markers,
|
||||
codex_home,
|
||||
&user_file,
|
||||
&active_user_file,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -356,6 +364,36 @@ pub async fn load_config_layers_state(
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_user_config_layer(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
user_file: &AbsolutePathBuf,
|
||||
profile: Option<&ProfileV2Name>,
|
||||
ignore_user_config: bool,
|
||||
strict_config: bool,
|
||||
) -> io::Result<ConfigLayerEntry> {
|
||||
let profile = profile.map(ToString::to_string);
|
||||
if ignore_user_config {
|
||||
return Ok(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
profile,
|
||||
},
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
));
|
||||
}
|
||||
|
||||
load_config_toml_for_required_layer(fs, user_file, strict_config, |config_toml| {
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
profile: profile.clone(),
|
||||
},
|
||||
config_toml,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn insert_layer_by_precedence(layers: &mut Vec<ConfigLayerEntry>, layer: ConfigLayerEntry) {
|
||||
match layers
|
||||
.iter()
|
||||
|
||||
+161
-50
@@ -5,12 +5,14 @@ use super::fingerprint::record_origins;
|
||||
use super::fingerprint::version_for_toml;
|
||||
use super::key_aliases::normalized_with_key_aliases;
|
||||
use super::merge::merge_toml_values;
|
||||
use crate::ProfileV2Name;
|
||||
use codex_app_server_protocol::ConfigLayer;
|
||||
use codex_app_server_protocol::ConfigLayerMetadata;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
@@ -33,6 +35,8 @@ impl From<LoaderOverrides> for ConfigLoadOptions {
|
||||
/// LoaderOverrides overrides managed configuration inputs (primarily for tests).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LoaderOverrides {
|
||||
pub user_config_path: Option<AbsolutePathBuf>,
|
||||
pub user_config_profile: Option<ProfileV2Name>,
|
||||
pub managed_config_path: Option<PathBuf>,
|
||||
pub system_config_path: Option<PathBuf>,
|
||||
pub system_requirements_path: Option<PathBuf>,
|
||||
@@ -52,6 +56,8 @@ impl LoaderOverrides {
|
||||
pub fn without_managed_config_for_tests() -> Self {
|
||||
let base = std::env::temp_dir().join("codex-config-tests");
|
||||
Self {
|
||||
user_config_path: None,
|
||||
user_config_profile: None,
|
||||
managed_config_path: Some(base.join("managed_config.toml")),
|
||||
system_config_path: Some(base.join("config.toml")),
|
||||
system_requirements_path: Some(base.join("requirements.toml")),
|
||||
@@ -69,10 +75,22 @@ impl LoaderOverrides {
|
||||
/// This is intended for tests that supply an explicit managed config fixture.
|
||||
pub fn with_managed_config_path_for_tests(managed_config_path: PathBuf) -> Self {
|
||||
Self {
|
||||
user_config_path: None,
|
||||
user_config_profile: None,
|
||||
managed_config_path: Some(managed_config_path),
|
||||
..Self::without_managed_config_for_tests()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_config_path(&self, codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {
|
||||
match self.user_config_path.as_ref() {
|
||||
Some(path) => Ok(path.clone()),
|
||||
None => Ok(AbsolutePathBuf::resolve_path_against_base(
|
||||
crate::CONFIG_TOML_FILE,
|
||||
codex_home,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -163,7 +181,7 @@ impl ConfigLayerEntry {
|
||||
match &self.name {
|
||||
ConfigLayerSource::Mdm { .. } => None,
|
||||
ConfigLayerSource::System { file } => file.parent(),
|
||||
ConfigLayerSource::User { file } => file.parent(),
|
||||
ConfigLayerSource::User { file, .. } => file.parent(),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
|
||||
ConfigLayerSource::SessionFlags => None,
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,
|
||||
@@ -196,7 +214,12 @@ pub struct ConfigLayerStack {
|
||||
/// later entries in the Vec override earlier ones.
|
||||
layers: Vec<ConfigLayerEntry>,
|
||||
|
||||
/// Index into [layers] of the user config layer, if any.
|
||||
/// Index into [layers] of the active user config layer, if any.
|
||||
///
|
||||
/// When profile config is active, there can be more than one user layer:
|
||||
/// the base `$CODEX_HOME/config.toml` layer followed by the profile override
|
||||
/// layer. This index points at the highest-precedence user layer because that
|
||||
/// is the writable layer for profile-aware edits.
|
||||
user_layer_index: Option<usize>,
|
||||
|
||||
/// Constraints that must be enforced when deriving a [Config] from the
|
||||
@@ -256,14 +279,61 @@ impl ConfigLayerStack {
|
||||
self.startup_warnings.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the raw user config layer, if any.
|
||||
/// Returns the active raw user config layer, if any.
|
||||
///
|
||||
/// This does not merge other config layers or apply any requirements.
|
||||
pub fn get_user_layer(&self) -> Option<&ConfigLayerEntry> {
|
||||
/// This does not merge other config layers or apply any requirements. When
|
||||
/// a profile-v2 layer is active, this returns that profile layer rather than
|
||||
/// the base `$CODEX_HOME/config.toml` layer because the active layer is the
|
||||
/// writable target for profile-aware edits.
|
||||
pub fn get_active_user_layer(&self) -> Option<&ConfigLayerEntry> {
|
||||
self.user_layer_index
|
||||
.and_then(|index| self.layers.get(index))
|
||||
}
|
||||
|
||||
pub fn get_user_config_file(&self) -> Option<&AbsolutePathBuf> {
|
||||
let layer = self.get_active_user_layer()?;
|
||||
let ConfigLayerSource::User { file, .. } = &layer.name else {
|
||||
return None;
|
||||
};
|
||||
Some(file)
|
||||
}
|
||||
|
||||
/// Returns all user config layers in the requested precedence order.
|
||||
///
|
||||
/// With profile-v2 enabled, `LowestPrecedenceFirst` returns the base user
|
||||
/// config before the profile overlay, while `HighestPrecedenceFirst` returns
|
||||
/// the profile overlay before the base user config.
|
||||
pub fn get_user_layers(
|
||||
&self,
|
||||
ordering: ConfigLayerStackOrdering,
|
||||
include_disabled: bool,
|
||||
) -> Vec<&ConfigLayerEntry> {
|
||||
self.get_layers(ordering, include_disabled)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the merged config from enabled user layers only.
|
||||
///
|
||||
/// When profile config is active, this includes the base user config followed
|
||||
/// by the profile override config.
|
||||
pub fn effective_user_config(&self) -> Option<TomlValue> {
|
||||
let user_layers = self.get_user_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
);
|
||||
if user_layers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut merged = TomlValue::Table(toml::map::Map::new());
|
||||
for layer in user_layers {
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
}
|
||||
Some(merged)
|
||||
}
|
||||
|
||||
pub fn requirements(&self) -> &ConfigRequirements {
|
||||
&self.requirements
|
||||
}
|
||||
@@ -272,54 +342,101 @@ impl ConfigLayerStack {
|
||||
&self.requirements_toml
|
||||
}
|
||||
|
||||
/// Creates a new [ConfigLayerStack] using the specified values to inject a
|
||||
/// "user layer" into the stack. If such a layer already exists, it is
|
||||
/// replaced; otherwise, it is inserted into the stack at the appropriate
|
||||
/// position based on precedence rules.
|
||||
/// Creates a new [ConfigLayerStack] using the specified values to inject one
|
||||
/// user layer into the stack. If such a layer already exists, it is replaced;
|
||||
/// otherwise, it is inserted into the stack at the appropriate position
|
||||
/// based on precedence rules. When the stack has both base and profile-v2
|
||||
/// user layers, this updates only the layer whose file matches
|
||||
/// `config_toml`.
|
||||
pub fn with_user_config(&self, config_toml: &AbsolutePathBuf, user_config: TomlValue) -> Self {
|
||||
self.with_user_layer(Some(ConfigLayerEntry::new(
|
||||
let profile = self.layers.iter().find_map(|layer| match &layer.name {
|
||||
ConfigLayerSource::User { file, profile } if file == config_toml => profile
|
||||
.as_deref()
|
||||
.and_then(|profile| profile.parse::<ProfileV2Name>().ok()),
|
||||
_ => None,
|
||||
});
|
||||
self.with_user_config_profile(config_toml, profile.as_ref(), user_config)
|
||||
}
|
||||
|
||||
pub fn with_user_config_profile(
|
||||
&self,
|
||||
config_toml: &AbsolutePathBuf,
|
||||
profile: Option<&ProfileV2Name>,
|
||||
user_config: TomlValue,
|
||||
) -> Self {
|
||||
let user_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: config_toml.clone(),
|
||||
profile: profile.map(ToString::to_string),
|
||||
},
|
||||
user_config,
|
||||
)))
|
||||
);
|
||||
|
||||
let mut layers = self.layers.clone();
|
||||
if let Some(index) = layers.iter().position(|layer| {
|
||||
matches!(
|
||||
&layer.name,
|
||||
ConfigLayerSource::User { file, .. } if file == config_toml
|
||||
)
|
||||
}) {
|
||||
layers.remove(index);
|
||||
}
|
||||
match layers
|
||||
.iter()
|
||||
.position(|layer| layer.name.precedence() > user_layer.name.precedence())
|
||||
{
|
||||
Some(index) => layers.insert(index, user_layer),
|
||||
None => layers.push(user_layer),
|
||||
}
|
||||
let user_layer_index = layers.iter().enumerate().rev().find_map(|(index, layer)| {
|
||||
if matches!(layer.name, ConfigLayerSource::User { .. }) {
|
||||
Some(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
Self {
|
||||
layers,
|
||||
user_layer_index,
|
||||
requirements: self.requirements.clone(),
|
||||
requirements_toml: self.requirements_toml.clone(),
|
||||
ignore_user_and_project_exec_policy_rules: self
|
||||
.ignore_user_and_project_exec_policy_rules,
|
||||
startup_warnings: self.startup_warnings.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new stack with the user layer copied from `other`, preserving
|
||||
/// every non-user layer already present in this stack.
|
||||
pub fn with_user_layer_from(&self, other: &Self) -> Self {
|
||||
self.with_user_layer(other.get_user_layer().cloned())
|
||||
}
|
||||
|
||||
fn with_user_layer(&self, user_layer: Option<ConfigLayerEntry>) -> Self {
|
||||
let mut layers = self.layers.clone();
|
||||
let user_layer_index = match (self.user_layer_index, user_layer) {
|
||||
(Some(index), Some(user_layer)) => {
|
||||
layers[index] = user_layer;
|
||||
Some(index)
|
||||
let user_layers = other
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut layers = self
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|layer| !matches!(layer.name, ConfigLayerSource::User { .. }))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for user_layer in user_layers {
|
||||
match layers
|
||||
.iter()
|
||||
.position(|layer| layer.name.precedence() > user_layer.name.precedence())
|
||||
{
|
||||
Some(index) => layers.insert(index, user_layer),
|
||||
None => layers.push(user_layer),
|
||||
}
|
||||
(Some(index), None) => {
|
||||
layers.remove(index);
|
||||
}
|
||||
let user_layer_index = layers.iter().enumerate().rev().find_map(|(index, layer)| {
|
||||
if matches!(layer.name, ConfigLayerSource::User { .. }) {
|
||||
Some(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
(None, Some(user_layer)) => {
|
||||
let user_layer_index = match layers
|
||||
.iter()
|
||||
.position(|layer| layer.name.precedence() > user_layer.name.precedence())
|
||||
{
|
||||
Some(index) => {
|
||||
layers.insert(index, user_layer);
|
||||
index
|
||||
}
|
||||
None => {
|
||||
layers.push(user_layer);
|
||||
layers.len() - 1
|
||||
}
|
||||
};
|
||||
Some(user_layer_index)
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
});
|
||||
Self {
|
||||
layers,
|
||||
user_layer_index,
|
||||
@@ -395,7 +512,7 @@ impl ConfigLayerStack {
|
||||
}
|
||||
|
||||
/// Ensures precedence ordering of config layers is correct. Returns the index
|
||||
/// of the user config layer, if any (at most one should exist).
|
||||
/// of the active user config layer, if any.
|
||||
fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<usize>> {
|
||||
if !layers.iter().map(|layer| &layer.name).is_sorted() {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -405,19 +522,13 @@ fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<
|
||||
}
|
||||
|
||||
// The previous check ensured `layers` is sorted by precedence, so now we
|
||||
// further verify that:
|
||||
// 1. There is at most one user config layer.
|
||||
// 2. Project layers are ordered from root to cwd.
|
||||
// further verify that project layers are ordered from root to cwd. Multiple
|
||||
// user layers are allowed so a profile override can layer on top of the base
|
||||
// user config.
|
||||
let mut user_layer_index: Option<usize> = None;
|
||||
let mut previous_project_dot_codex_folder: Option<&AbsolutePathBuf> = None;
|
||||
for (index, layer) in layers.iter().enumerate() {
|
||||
if matches!(layer.name, ConfigLayerSource::User { .. }) {
|
||||
if user_layer_index.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"multiple user config layers found",
|
||||
));
|
||||
}
|
||||
user_layer_index = Some(index);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_user_config_path(temp_dir: &TempDir, file_name: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path().join(file_name))
|
||||
.expect("test user config path should be absolute")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origins_use_canonical_key_aliases() {
|
||||
@@ -32,3 +38,104 @@ no_memories_if_mcp_or_web_search = true
|
||||
"legacy key should be canonicalized before origin recording"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_user_layer_is_highest_precedence_user_layer() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
let base_file = test_user_config_path(&temp_dir, "config.toml");
|
||||
let profile_file = test_user_config_path(&temp_dir, "work.config.toml");
|
||||
let base_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: base_file,
|
||||
profile: None,
|
||||
},
|
||||
toml::from_str(
|
||||
r#"
|
||||
model = "base"
|
||||
approval_policy = "on-failure"
|
||||
"#,
|
||||
)
|
||||
.expect("base config"),
|
||||
);
|
||||
let profile_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: profile_file.clone(),
|
||||
profile: Some("work".to_string()),
|
||||
},
|
||||
toml::from_str(r#"model = "profile""#).expect("profile config"),
|
||||
);
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![base_layer, profile_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("multiple user layers should be valid");
|
||||
|
||||
assert_eq!(stack.get_user_config_file(), Some(&profile_file));
|
||||
assert_eq!(
|
||||
stack
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("model")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("profile")
|
||||
);
|
||||
assert_eq!(
|
||||
stack
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("approval_policy")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("on-failure")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_user_config_updates_matching_user_layer_without_replacing_active_profile() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
let base_file = test_user_config_path(&temp_dir, "config.toml");
|
||||
let profile_file = test_user_config_path(&temp_dir, "work.config.toml");
|
||||
let base_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: base_file.clone(),
|
||||
profile: None,
|
||||
},
|
||||
toml::from_str(r#"model = "base""#).expect("base config"),
|
||||
);
|
||||
let profile_layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: profile_file.clone(),
|
||||
profile: Some("work".to_string()),
|
||||
},
|
||||
toml::from_str(r#"approval_policy = "on-failure""#).expect("profile config"),
|
||||
);
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![base_layer, profile_layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("multiple user layers should be valid");
|
||||
|
||||
let updated = stack.with_user_config(
|
||||
&base_file,
|
||||
toml::from_str(r#"model = "updated-base""#).expect("updated base config"),
|
||||
);
|
||||
|
||||
assert_eq!(updated.get_user_config_file(), Some(&profile_file));
|
||||
assert_eq!(
|
||||
updated
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("model")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("updated-base")
|
||||
);
|
||||
assert_eq!(
|
||||
updated
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("approval_policy")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("on-failure")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user