fix(config): correct cross-platform paths and v8 config updates

- Extend OAuth directory recovery to Windows and Linux
- Align credential path resolution and log directory selection with the core
- Update v8 TLS and logging fields without requiring a version marker
- Support flow-style YAML mappings and preserve null legacy TLS fields
- Add regression tests for path recovery and config updates
This commit is contained in:
lzt404 committed 2026-09-28 00:22:03 +08:00
1 parent 6dd8646afc
commit 7c5b65a153
6 files changed
+445 -172

No files matched your search

+91 -41
View File
@@ -837,26 +837,78 @@ pub(crate) fn fixed_oauth_dir() -> Result<PathBuf, String> {
Ok(core_base_dir()?.join(OAUTH_DIR_NAME))
}
pub(crate) fn auth_dir_path_for_core(auth_dir: &str, install_dir: &Path) -> PathBuf {
if auth_dir.trim() == DEFAULT_AUTH_DIR {
return install_dir
.parent()
.map(|parent| parent.join(OAUTH_DIR_NAME))
.unwrap_or_else(|| install_dir.join(auth_dir));
}
let auth_dir = PathBuf::from(auth_dir);
if auth_dir.is_absolute() {
auth_dir
pub(crate) fn auth_dir_path_for_core(auth_dir: &str, install_dir: &Path) -> Result<PathBuf, String> {
let variable = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let user_home = env::var_os(variable).filter(|value| !value.is_empty()).map(PathBuf::from);
auth_dir_path_for_core_with_home(auth_dir, install_dir, user_home.as_deref())
}
pub(crate) fn auth_dir_path_for_core_with_home(
auth_dir: &str,
install_dir: &Path,
user_home: Option<&Path>,
) -> Result<PathBuf, String> {
let auth_dir = if auth_dir.is_empty() { "~/.cli-proxy-api" } else { auth_dir };
let path = if let Some(remainder) = auth_dir.strip_prefix('~') {
let home = user_home.ok_or_else(|| "Unable to determine user home for credentials directory".to_string())?;
home.join(remainder.trim_start_matches(['/', '\\']).replace('\\', "/"))
} else {
install_dir.join(auth_dir)
PathBuf::from(auth_dir)
};
Ok(resolve_core_relative_path(&path, install_dir))
}
fn resolve_core_relative_path(path: &Path, install_dir: &Path) -> PathBuf {
normalize_path_lexically(&if path.is_absolute() {
path.to_path_buf()
} else {
install_dir.join(path)
})
}
pub(crate) fn core_logs_dir_path(auth_dir: &str, install_dir: &Path) -> Result<PathBuf, String> {
let writable_base = ["WRITABLE_PATH", "writable_path"].into_iter().find_map(|key| {
env::var(key).ok().filter(|value| !value.trim().is_empty())
});
core_logs_dir_path_with_base(auth_dir, install_dir, writable_base.as_deref())
}
pub(crate) fn core_logs_dir_path_with_base(
auth_dir: &str,
install_dir: &Path,
writable_base: Option<&str>,
) -> Result<PathBuf, String> {
if let Some(base) = writable_base.map(str::trim).filter(|base| !base.is_empty()) {
return Ok(resolve_core_relative_path(&Path::new(base).join("logs"), install_dir));
}
let local_logs = install_dir.join("logs");
if directory_is_writable(&local_logs) {
return Ok(normalize_path_lexically(&local_logs));
}
Ok(auth_dir_path_for_core(auth_dir, install_dir)?.join("logs"))
}
pub(crate) fn core_logs_dir_path(auth_dir: &str, install_dir: &Path) -> PathBuf {
auth_dir_path_for_core(auth_dir, install_dir).join("logs")
fn directory_is_writable(path: &Path) -> bool {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
if !path.is_dir() {
return false;
}
let probe = path.join(format!(
".cpa-gui-log-probe-{}-{}-{}",
std::process::id(),
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos(),
SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
let Ok(file) = File::options().write(true).create_new(true).open(&probe) else {
return false;
};
drop(file);
let _ = fs::remove_file(probe);
true
}
#[cfg(any(target_os = "macos", test))]
fn normalize_path_lexically(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
@@ -1063,7 +1115,7 @@ pub(crate) fn migrate_auth_dir_from_macos_app_bundle(
install_dir: &Path,
persistent_auth_dir: &Path,
) -> Result<bool, String> {
let source = normalize_path_lexically(&auth_dir_path_for_core(&config.auth_dir, install_dir));
let source = auth_dir_path_for_core(&config.auth_dir, install_dir)?;
if !auth_dir_is_inside_macos_app_bundle(&source) {
return Ok(false);
}
@@ -1080,7 +1132,6 @@ pub(crate) fn migrate_auth_dir_from_macos_app_bundle(
Ok(true)
}
#[cfg(any(target_os = "macos", test))]
fn auth_directory_has_json_files(path: &Path) -> Result<bool, String> {
let entries = match fs::read_dir(path) {
Ok(entries) => entries,
@@ -1110,14 +1161,12 @@ fn auth_directory_has_json_files(path: &Path) -> Result<bool, String> {
Ok(false)
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn recover_macos_relative_oauth_dir(
pub(crate) fn recover_relative_oauth_dir(
config: &mut GuiConfigFile,
install_dir: &Path,
persistent_auth_dir: &Path,
) -> Result<bool, String> {
let current_auth_dir =
normalize_path_lexically(&auth_dir_path_for_core(&config.auth_dir, install_dir));
let current_auth_dir = auth_dir_path_for_core(&config.auth_dir, install_dir)?;
if current_auth_dir != normalize_path_lexically(&install_dir.join(OAUTH_DIR_NAME)) {
return Ok(false);
}
@@ -1130,23 +1179,18 @@ pub(crate) fn recover_macos_relative_oauth_dir(
Ok(true)
}
#[cfg(target_os = "macos")]
fn migrate_packaged_macos_auth_dir(config: &mut GuiConfigFile) -> Result<bool, String> {
let install_dir = core_install_dir()?;
let persistent_auth_dir = fixed_oauth_dir()?;
migrate_macos_auth_dir_at(config, &install_dir, &persistent_auth_dir)
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn migrate_macos_auth_dir_at(
pub(crate) fn migrate_auth_dir_at(
config: &mut GuiConfigFile,
install_dir: &Path,
persistent_auth_dir: &Path,
) -> Result<bool, String> {
let mut candidate = config.clone();
#[cfg(target_os = "macos")]
let migrated =
migrate_auth_dir_from_macos_app_bundle(&mut candidate, install_dir, persistent_auth_dir)?;
let recovered = recover_macos_relative_oauth_dir(&mut candidate, install_dir, persistent_auth_dir)?;
#[cfg(not(target_os = "macos"))]
let migrated = false;
let recovered = recover_relative_oauth_dir(&mut candidate, install_dir, persistent_auth_dir)?;
if migrated || recovered {
patch_core_auth_dir_at(&install_dir.join(CORE_CONFIG_FILE), &candidate.auth_dir)
.map_err(|error| format!("Failed to update kernel OAuth directory: {error}"))?;
@@ -1411,7 +1455,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
}
if !config.auth_dir.trim().is_empty() {
let install_dir = core_install_dir()?;
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &install_dir);
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &install_dir)?;
fs::create_dir_all(&auth_dir)
.map_err(|error| format!("Failed to create credentials directory {}: {error}", path_to_string(&auth_dir)))?;
}
@@ -1651,6 +1695,16 @@ pub(crate) fn is_loopback_host(host: &str) -> bool {
}
pub(crate) fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result<bool, String> {
let install_dir = core_install_dir()?;
let persistent_auth_dir = fixed_oauth_dir()?;
sanitize_gui_config_at(config, &install_dir, &persistent_auth_dir)
}
pub(crate) fn sanitize_gui_config_at(
config: &mut GuiConfigFile,
install_dir: &Path,
persistent_auth_dir: &Path,
) -> Result<bool, String> {
let mut changed = false;
let normalized_locale = normalize_app_locale(&config.locale);
if config.locale != normalized_locale {
@@ -1681,18 +1735,14 @@ pub(crate) fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result<bool, St
config.allow_lan = allow_lan;
changed = true;
}
#[cfg(target_os = "macos")]
{
match migrate_packaged_macos_auth_dir(config) {
Ok(migrated) => changed |= migrated,
Err(error) => {
eprintln!("Failed to recover macOS OAuth directory; keeping existing configuration: {error}");
}
match migrate_auth_dir_at(config, install_dir, persistent_auth_dir) {
Ok(migrated) => changed |= migrated,
Err(error) => {
eprintln!("Failed to recover OAuth directory; keeping existing configuration: {error}");
}
}
let legacy_default_auth_dir = fixed_oauth_dir()?;
if config.auth_dir.trim().is_empty()
|| Path::new(config.auth_dir.trim()) == legacy_default_auth_dir
|| Path::new(config.auth_dir.trim()) == persistent_auth_dir
{
config.auth_dir = DEFAULT_AUTH_DIR.to_string();
changed = true;
@@ -1958,7 +2008,7 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
}
#[cfg(target_os = "macos")]
{
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &core_install_dir()?);
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &core_install_dir()?)?;
if auth_dir_is_inside_macos_app_bundle(&auth_dir) {
return Err("OAuth credentials directory cannot be inside a macOS application bundle".to_string());
}
+120 -90
View File
@@ -185,23 +185,29 @@ pub(crate) fn patch_core_tls_settings_yaml(
settings: &CoreTlsSettings,
) -> Result<Option<String>, String> {
patch_core_yaml_document(content, |document| {
let v8 = core_config_uses_v8(document);
let root = document.as_mapping()
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let v8 = core_config_uses_v8(document)
|| nested_yaml_value(root, &["server", "tls"]).is_some();
let has_legacy_mapping = yaml_mapping_value(root, "tls")
.is_some_and(serde_norway::Value::is_mapping);
let mut changed = false;
changed |= set_core_yaml_path_value(
document,
if v8 { &["server", "tls", "enable"] } else { &["tls", "enable"] },
serde_norway::Value::Bool(settings.enabled),
)?;
changed |= set_core_yaml_path_value(
document,
if v8 { &["server", "tls", "cert"] } else { &["tls", "cert"] },
serde_norway::Value::String(settings.cert.clone()),
)?;
changed |= set_core_yaml_path_value(
document,
if v8 { &["server", "tls", "key"] } else { &["tls", "key"] },
serde_norway::Value::String(settings.key.clone()),
)?;
for (key, value) in [
("enable", serde_norway::Value::Bool(settings.enabled)),
("cert", serde_norway::Value::String(settings.cert.clone())),
("key", serde_norway::Value::String(settings.key.clone())),
] {
let legacy_path = ["tls", key];
let v8_path = ["server", "tls", key];
changed |= set_core_yaml_path_value(
document,
if v8 { &v8_path } else { &legacy_path },
value.clone(),
)?;
if v8 && has_legacy_mapping {
changed |= set_core_yaml_path_value(document, &["tls", key], value)?;
}
}
Ok(changed)
})
}
@@ -431,7 +437,6 @@ pub(crate) fn patch_core_management_secret_key(secret_key: &str) -> Result<(), S
})
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn patch_core_auth_dir_at(config_path: &Path, auth_dir: &str) -> Result<(), String> {
patch_existing_core_config_at(config_path, |document| {
set_core_yaml_auth_dir(document, auth_dir)
@@ -442,26 +447,12 @@ pub(crate) fn set_core_yaml_auth_dir(
document: &mut serde_norway::Value,
auth_dir: &str,
) -> Result<bool, String> {
let root = document
.as_mapping()
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let nested = core_config_uses_v8(document)
|| nested_yaml_value(root, &["oauth", "auth-dir"]).is_some();
let has_legacy = yaml_mapping_value(root, "auth-dir").is_some();
let value = serde_norway::Value::String(auth_dir.to_string());
let mut changed = set_core_yaml_path_value(
set_core_yaml_schema_value(
document,
if nested {
&["oauth", "auth-dir"]
} else {
&["auth-dir"]
},
value.clone(),
)?;
if nested && has_legacy {
changed |= set_core_yaml_path_value(document, &["auth-dir"], value)?;
}
Ok(changed)
&["auth-dir"],
&["oauth", "auth-dir"],
serde_norway::Value::String(auth_dir.to_string()),
)
}
pub(crate) fn patch_core_plugins_enabled(enabled: bool) -> Result<(), String> {
@@ -477,63 +468,64 @@ pub(crate) fn patch_core_plugins_enabled(enabled: bool) -> Result<(), String> {
pub(crate) fn patch_core_request_log(enabled: bool) -> Result<(), String> {
patch_existing_core_config(|document| {
let v8 = core_config_uses_v8(document);
set_core_yaml_path_value(
set_core_yaml_schema_value(
document,
if v8 { &["observability", "logs", "request-log"] } else { &["request-log"] },
&["request-log"],
&["observability", "logs", "request-log"],
serde_norway::Value::Bool(enabled),
)
})
}
pub(crate) fn patch_core_logging_settings(settings: &CoreConfigSettings) -> Result<(), String> {
let settings = settings.clone();
patch_existing_core_config(move |document| {
let v8 = core_config_uses_v8(document);
let mut changed = false;
for (key, value) in [
("debug", serde_norway::Value::Bool(settings.debug)),
(
"commercial-mode",
serde_norway::Value::Bool(settings.commercial_mode),
),
(
"logging-to-file",
serde_norway::Value::Bool(settings.logging_to_file),
),
(
"logs-max-total-size-mb",
serde_norway::to_value(settings.logs_max_total_size_mb)
.map_err(|err| format!("Failed to serialize log size limit: {err}"))?,
),
(
"error-logs-max-files",
serde_norway::to_value(settings.error_logs_max_files)
.map_err(|err| format!("Failed to serialize error log retention count: {err}"))?,
),
(
"usage-statistics-enabled",
serde_norway::Value::Bool(settings.usage_statistics_enabled),
),
(
"redis-usage-queue-retention-seconds",
serde_norway::to_value(settings.redis_usage_queue_retention_seconds)
.map_err(|err| format!("Failed to serialize Redis usage queue retention: {err}"))?,
),
] {
let path: &[&str] = if v8 {
match key {
"commercial-mode" => &["server", "commercial-mode"],
"usage-statistics-enabled" | "redis-usage-queue-retention-seconds" => &["observability", "usage", key],
_ => &["observability", "logs", key],
}
} else {
&[key]
};
changed |= set_core_yaml_path_value(document, path, value)?;
}
Ok(changed)
})
patch_existing_core_config(|document| apply_core_logging_settings(document, settings))
}
pub(crate) fn apply_core_logging_settings(
document: &mut serde_norway::Value,
settings: &CoreConfigSettings,
) -> Result<bool, String> {
let mut changed = false;
for (key, value) in [
("debug", serde_norway::Value::Bool(settings.debug)),
(
"commercial-mode",
serde_norway::Value::Bool(settings.commercial_mode),
),
(
"logging-to-file",
serde_norway::Value::Bool(settings.logging_to_file),
),
(
"logs-max-total-size-mb",
serde_norway::to_value(settings.logs_max_total_size_mb)
.map_err(|err| format!("Failed to serialize log size limit: {err}"))?,
),
(
"error-logs-max-files",
serde_norway::to_value(settings.error_logs_max_files)
.map_err(|err| format!("Failed to serialize error log retention count: {err}"))?,
),
(
"usage-statistics-enabled",
serde_norway::Value::Bool(settings.usage_statistics_enabled),
),
(
"redis-usage-queue-retention-seconds",
serde_norway::to_value(settings.redis_usage_queue_retention_seconds)
.map_err(|err| format!("Failed to serialize Redis usage queue retention: {err}"))?,
),
] {
let path: &[&str] = match key {
"commercial-mode" => &["server", "commercial-mode"],
"usage-statistics-enabled" | "redis-usage-queue-retention-seconds" => {
&["observability", "usage", key]
}
_ => &["observability", "logs", key],
};
changed |= set_core_yaml_schema_value(document, &[key], path, value)?;
}
Ok(changed)
}
pub(crate) fn patch_core_routing_strategy(strategy: &str) -> Result<(), String> {
@@ -693,7 +685,7 @@ pub(crate) fn render_yaml_value_changes(
}
for (parent_path, entries) in missing_nested_groups {
if let Some(updated_content) =
insert_yaml_block_mapping_values_at_path(&editable_content, &parent_path, &entries)?
insert_yaml_mapping_values_at_path(&editable_content, &parent_path, &entries)?
{
editable_content = updated_content;
} else {
@@ -894,11 +886,38 @@ pub(crate) fn yaml_value_at_path<'a>(
})
}
pub(crate) fn insert_yaml_block_mapping_values_at_path(
pub(crate) fn insert_yaml_mapping_values_at_path(
content: &str,
parent_path: &[String],
entries: &[(String, serde_norway::Value)],
) -> Result<Option<String>, String> {
let file = content.parse::<yaml_edit::YamlFile>()
.map_err(|error| format!("Failed to parse editable kernel configuration: {error}"))?;
let document = file.document()
.ok_or_else(|| "Kernel configuration has no YAML document".to_string())?;
let root = document.as_mapping()
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let node = yaml_edit_node_at_path(&root, parent_path);
if let Some(mapping) = node.as_ref().and_then(|node| node.as_mapping())
.filter(|mapping| mapping.is_flow_style())
{
let range = mapping.byte_range();
let start = range.start as usize;
let opening = content[start..range.end as usize].find('{')
.ok_or_else(|| "Flow mapping has no opening brace".to_string())?;
let insertion_point = start + opening + 1;
let fields = entries.iter().map(|(key, value)| {
serde_json::to_string(value)
.map(|value| format!("{}: {value}", render_yaml_mapping_key(key)))
.map_err(|error| format!("Failed to serialize kernel configuration value: {error}"))
}).collect::<Result<Vec<_>, _>>()?.join(", ");
let separator = if mapping.is_empty() { " " } else { ", " };
return Ok(Some(format!(
"{} {fields}{separator}{}",
&content[..insertion_point],
&content[insertion_point..]
)));
}
let mut offset = 0;
let mut parent_end = None;
let mut parent_indent = 0;
@@ -1243,8 +1262,19 @@ pub(crate) fn set_core_yaml_schema_value(
v8_path: &[&str],
value: serde_norway::Value,
) -> Result<bool, String> {
let path = if core_config_uses_v8(document) { v8_path } else { legacy_path };
set_core_yaml_path_value(document, path, value)
let root = document.as_mapping()
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let v8 = core_config_uses_v8(document) || nested_yaml_value(root, v8_path).is_some();
let has_legacy = nested_yaml_value(root, legacy_path).is_some();
let mut changed = set_core_yaml_path_value(
document,
if v8 { v8_path } else { legacy_path },
value.clone(),
)?;
if v8 && has_legacy {
changed |= set_core_yaml_path_value(document, legacy_path, value)?;
}
Ok(changed)
}
pub(crate) fn patch_core_api_keys_yaml(
+4 -4
View File
@@ -1388,8 +1388,8 @@ impl std::fmt::Display for CoreStartupFailure {
}
}
pub(crate) fn core_start_log_path(install_dir: &Path, auth_dir: &str) -> PathBuf {
core_logs_dir_path(auth_dir, install_dir).join("core-start-output.log")
pub(crate) fn core_start_log_path(install_dir: &Path, auth_dir: &str) -> Result<PathBuf, String> {
Ok(core_logs_dir_path(auth_dir, install_dir)?.join("core-start-output.log"))
}
pub(crate) fn core_start_stdio(log_path: &Path) -> io::Result<(Stdio, Stdio)> {
@@ -1510,7 +1510,7 @@ pub(crate) fn start_core_process_inner(
let gui_config = &resolved_config;
let install_dir = core_install_dir()?;
if !gui_config.auth_dir.trim().is_empty() {
let auth_dir = auth_dir_path_for_core(&gui_config.auth_dir, &install_dir);
let auth_dir = auth_dir_path_for_core(&gui_config.auth_dir, &install_dir)?;
fs::create_dir_all(&auth_dir)
.map_err(|error| format!("Failed to create credentials directory {}: {error}", path_to_string(&auth_dir)))?;
}
@@ -1534,7 +1534,7 @@ pub(crate) fn start_core_process_inner(
let config_path = merge_core_config_for_start(&install_dir, gui_config)?;
let config_path = path_to_string(&config_path);
let log_path = core_start_log_path(&install_dir, &gui_config.auth_dir);
let log_path = core_start_log_path(&install_dir, &gui_config.auth_dir)?;
let start_once = || {
start_core_process_once(
process_state,
+4 -4
View File
@@ -133,7 +133,7 @@ pub(crate) fn open_auth_files_directory(
) -> Result<(), String> {
let config = gui_config_state.snapshot()?;
let install_dir = core_install_dir()?;
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &install_dir);
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &install_dir)?;
fs::create_dir_all(&auth_dir)
.map_err(|error| format!("Failed to create credentials directory {}: {error}", path_to_string(&auth_dir)))?;
open_directory_in_file_manager(&auth_dir)
@@ -145,7 +145,7 @@ pub(crate) fn open_core_logs_directory(
) -> Result<(), String> {
let config = gui_config_state.snapshot()?;
let install_dir = core_install_dir()?;
let logs_dir = core_logs_dir_path(&config.auth_dir, &install_dir);
let logs_dir = core_logs_dir_path(&config.auth_dir, &install_dir)?;
fs::create_dir_all(&logs_dir)
.map_err(|error| format!("Failed to create log directory {}: {error}", path_to_string(&logs_dir)))?;
@@ -474,7 +474,7 @@ mod tests {
let install_dir = base_dir.join("cpa-core");
assert_eq!(
core_logs_dir_path("../oauth", &install_dir),
core_logs_dir_path("../oauth", &install_dir).unwrap(),
base_dir.join("oauth").join("logs")
);
}
@@ -485,7 +485,7 @@ mod tests {
let auth_dir = PathBuf::from("custom-auth");
assert_eq!(
core_logs_dir_path(auth_dir.to_str().unwrap(), &install_dir),
core_logs_dir_path(auth_dir.to_str().unwrap(), &install_dir).unwrap(),
install_dir.join(auth_dir).join("logs")
);
}
+224 -31
View File
@@ -191,7 +191,7 @@ fn default_auth_directory_is_relative_and_legacy_absolute_value_is_migrated() {
let base_dir = agent_test_home("relative-default-auth-dir");
let install_dir = base_dir.join("cpa-core");
assert_eq!(
auth_dir_path_for_core(DEFAULT_AUTH_DIR, &install_dir),
auth_dir_path_for_core(DEFAULT_AUTH_DIR, &install_dir).unwrap(),
base_dir.join(OAUTH_DIR_NAME)
);
@@ -205,8 +205,161 @@ fn default_auth_directory_is_relative_and_legacy_absolute_value_is_migrated() {
}
#[test]
fn macos_relative_oauth_dir_recovers_existing_persistent_credentials() {
let root = agent_test_home("macos-relative-oauth-dir");
fn path_configuration_expands_auth_home() {
let variable = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let user_home = PathBuf::from(env::var_os(variable).unwrap());
let install_dir = agent_test_home("auth-home-path").join("cpa-core");
assert_eq!(
auth_dir_path_for_core("~/.cli-proxy-api", &install_dir).unwrap(),
user_home.join(".cli-proxy-api")
);
fs::remove_dir_all(install_dir.parent().unwrap()).unwrap();
}
#[test]
fn path_configuration_uses_existing_core_logs() {
let root = agent_test_home("existing-core-logs");
let install_dir = root.join("cpa-core");
let logs_dir = install_dir.join("logs");
fs::create_dir_all(&logs_dir).unwrap();
fs::write(logs_dir.join("main.log"), b"existing-log").unwrap();
assert_eq!(core_logs_dir_path_with_base(DEFAULT_AUTH_DIR, &install_dir, None).unwrap(), logs_dir);
assert_eq!(fs::read(logs_dir.join("main.log")).unwrap(), b"existing-log");
assert_eq!(fs::read_dir(&logs_dir).unwrap().count(), 1);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn path_configuration_respects_log_base_and_fallbacks() {
let root = agent_test_home("core-log-base");
let install_dir = root.join("cpa-core");
fs::create_dir_all(&install_dir).unwrap();
let fallback = root.join("oauth").join("logs");
assert_eq!(
core_logs_dir_path_with_base(DEFAULT_AUTH_DIR, &install_dir, None).unwrap(),
fallback
);
fs::write(install_dir.join("logs"), b"not a directory").unwrap();
assert_eq!(
core_logs_dir_path_with_base(DEFAULT_AUTH_DIR, &install_dir, None).unwrap(),
fallback
);
fs::remove_file(install_dir.join("logs")).unwrap();
fs::create_dir(install_dir.join("logs")).unwrap();
for base in [" ../shared logs/./ ".to_string(), path_to_string(&root.join("shared logs"))] {
assert_eq!(
core_logs_dir_path_with_base(DEFAULT_AUTH_DIR, &install_dir, Some(&base)).unwrap(),
root.join("shared logs").join("logs")
);
}
fs::remove_dir_all(root).unwrap();
}
#[test]
fn path_configuration_normalizes_auth_paths_and_rejects_missing_home() {
let root = agent_test_home("core-auth-normalization");
let install_dir = root.join("cpa-core");
let user_home = root.join("home");
for auth_dir in ["~/.cli-proxy-api", r"~\.cli-proxy-api", ""] {
assert_eq!(
auth_dir_path_for_core_with_home(auth_dir, &install_dir, Some(&user_home)).unwrap(),
user_home.join(".cli-proxy-api")
);
}
assert_eq!(
auth_dir_path_for_core_with_home("~", &install_dir, Some(&user_home)).unwrap(),
user_home
);
assert_eq!(
auth_dir_path_for_core("missing/../oauth/./", &install_dir).unwrap(),
install_dir.join("oauth")
);
assert!(auth_dir_path_for_core_with_home("~/oauth", &install_dir, None).is_err());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn path_configuration_writes_effective_tls_fields_without_version_marker() {
let settings = CoreTlsSettings {
enabled: true,
cert: "certs/new.crt".to_string(),
key: "certs/new.key".to_string(),
};
for input in [
"server:\n tls: {enable: true, cert: old.crt, key: old.key}\n",
"server: {tls: {enable: false}}\n",
"tls: {enable: false, cert: legacy.crt, key: legacy.key}\nserver: {tls: {enable: false}}\n",
] {
let patched = patch_core_tls_settings_yaml(input, &settings).unwrap().unwrap();
let document: serde_norway::Value = serde_norway::from_str(&patched).unwrap();
let effective = core_tls_settings_from_value(&document).unwrap();
assert!(effective.enabled);
assert_eq!(effective.cert, settings.cert);
assert_eq!(effective.key, settings.key);
if let Some(legacy) = document.get("tls") {
assert_eq!(legacy["cert"].as_str(), Some(settings.cert.as_str()));
}
assert!(patch_core_tls_settings_yaml(&patched, &settings).unwrap().is_none());
}
}
#[test]
fn tls_patch_preserves_null_legacy_fields_with_v8_settings() {
let settings = CoreTlsSettings {
enabled: false,
cert: "certs/new.crt".to_string(),
key: "certs/new.key".to_string(),
};
for version_marker in ["config-version: 8\n", ""] {
let input = format!(
"{version_marker}tls: null\nserver:\n tls:\n enable: true\n cert: old.crt\n key: old.key\n"
);
let patched = patch_core_tls_settings_yaml(&input, &settings)
.unwrap()
.expect("TLS settings should change");
let document: serde_norway::Value = serde_norway::from_str(&patched).unwrap();
assert_eq!(document.get("tls"), Some(&serde_norway::Value::Null));
let effective = core_tls_settings_from_value(&document).unwrap();
assert_eq!(effective.enabled, settings.enabled);
assert_eq!(effective.cert, settings.cert);
assert_eq!(effective.key, settings.key);
assert!(patch_core_tls_settings_yaml(&patched, &settings)
.unwrap()
.is_none());
}
}
#[test]
fn path_configuration_writes_effective_logging_fields_without_version_marker() {
let config = GuiConfigFile {
logging_to_file: true,
logs_max_total_size_mb: 123,
..GuiConfigFile::default()
};
let settings = CoreConfigSettings::from(&config);
for input in [
"observability:\n logs: {logging-to-file: false, logs-max-total-size-mb: 0}\n",
"logging-to-file: false\nobservability:\n logs: {logging-to-file: false}\n",
] {
for patched in [
apply_gui_managed_settings(input, &config).unwrap(),
patch_core_yaml_document(input, |document| apply_core_logging_settings(document, &settings))
.unwrap().unwrap(),
] {
let document: serde_norway::Value = serde_norway::from_str(&patched).unwrap();
let effective = core_config_settings_from_value(&document).unwrap();
assert!(effective.logging_to_file);
assert_eq!(effective.logs_max_total_size_mb, 123);
if let Some(legacy) = document.get("logging-to-file") {
assert_eq!(legacy.as_bool(), Some(true));
}
}
}
}
#[test]
fn relative_oauth_dir_recovers_existing_persistent_credentials() {
let root = agent_test_home("relative-oauth-dir");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
let misplaced_auth_dir = install_dir.join(OAUTH_DIR_NAME);
@@ -224,11 +377,11 @@ fn macos_relative_oauth_dir_recovers_existing_persistent_credentials() {
};
assert!(
recover_macos_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap()
recover_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, DEFAULT_AUTH_DIR);
assert_eq!(
auth_dir_path_for_core(&config.auth_dir, &install_dir),
auth_dir_path_for_core(&config.auth_dir, &install_dir).unwrap(),
persistent_auth_dir
);
assert_eq!(
@@ -241,8 +394,8 @@ fn macos_relative_oauth_dir_recovers_existing_persistent_credentials() {
}
#[test]
fn macos_oauth_recovery_accepts_equivalent_directory_paths() {
let root = agent_test_home("macos-oauth-equivalent-paths");
fn oauth_recovery_accepts_equivalent_directory_paths() {
let root = agent_test_home("oauth-equivalent-paths");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
fs::create_dir_all(&persistent_auth_dir).unwrap();
@@ -252,13 +405,17 @@ fn macos_oauth_recovery_accepts_equivalent_directory_paths() {
"oauth/./".to_string(),
"../cpa-core/oauth".to_string(),
path_to_string(&install_dir.join(OAUTH_DIR_NAME)),
#[cfg(windows)]
r".\oauth".to_string(),
#[cfg(windows)]
r"..\cpa-core\oauth".to_string(),
] {
let mut config = GuiConfigFile {
auth_dir: auth_dir.clone(),
..GuiConfigFile::default()
};
assert!(
recover_macos_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap(),
recover_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap(),
"failed to recover {auth_dir}"
);
assert_eq!(config.auth_dir, DEFAULT_AUTH_DIR);
@@ -267,8 +424,8 @@ fn macos_oauth_recovery_accepts_equivalent_directory_paths() {
}
#[test]
fn macos_oauth_recovery_preserves_credentials_behind_parent_components() {
let root = agent_test_home("macos-oauth-parent-components");
fn oauth_recovery_preserves_credentials_behind_parent_components() {
let root = agent_test_home("oauth-parent-components");
let install_dir = root.join("cpa-core");
let active_auth_dir = install_dir.join(OAUTH_DIR_NAME);
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
@@ -281,10 +438,12 @@ fn macos_oauth_recovery_preserves_credentials_behind_parent_components() {
for auth_dir in [
"missing/../oauth".to_string(),
path_to_string(&install_dir.join("missing").join("..").join(OAUTH_DIR_NAME)),
#[cfg(windows)]
r"missing\..\oauth".to_string(),
] {
#[cfg(unix)]
assert_eq!(
fs::read_dir(auth_dir_path_for_core(&auth_dir, &install_dir))
fs::read_dir(install_dir.join(&auth_dir))
.unwrap_err()
.kind(),
io::ErrorKind::NotFound
@@ -294,7 +453,7 @@ fn macos_oauth_recovery_preserves_credentials_behind_parent_components() {
..GuiConfigFile::default()
};
assert!(
!migrate_macos_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
!migrate_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, auth_dir);
}
@@ -327,14 +486,14 @@ fn oauth_directory_write_updates_the_effective_v8_field_without_version_marker()
}
#[test]
fn macos_oauth_recovery_survives_config_reload_and_v8_upgrade() {
fn oauth_recovery_survives_config_reload_and_v8_upgrade() {
for input in [
"auth-dir: oauth\n",
"config-version: 8\noauth:\n auth-dir: oauth\n",
"oauth:\n auth-dir: ./oauth\n",
"auth-dir: ../oauth\noauth: {auth-dir: oauth}\n",
] {
let root = agent_test_home("macos-oauth-recovery-reload");
let root = agent_test_home("oauth-recovery-reload");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
let core_config_path = install_dir.join(CORE_CONFIG_FILE);
@@ -355,7 +514,7 @@ fn macos_oauth_recovery_survives_config_reload_and_v8_upgrade() {
write_gui_config_to_path(&config, &gui_config_path).unwrap();
assert!(
migrate_macos_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
sanitize_gui_config_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
let patched = fs::read_to_string(&core_config_path).unwrap();
assert!(patched.contains("# user comment"));
@@ -373,7 +532,7 @@ fn macos_oauth_recovery_survives_config_reload_and_v8_upgrade() {
toml::from_str(&fs::read_to_string(&gui_config_path).unwrap()).unwrap();
assert_eq!(reloaded.auth_dir, DEFAULT_AUTH_DIR);
assert!(
!migrate_macos_auth_dir_at(&mut reloaded, &install_dir, &persistent_auth_dir).unwrap()
!sanitize_gui_config_at(&mut reloaded, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(fs::read_to_string(&core_config_path).unwrap(), patched);
@@ -391,7 +550,7 @@ fn macos_oauth_recovery_survives_config_reload_and_v8_upgrade() {
let effective = core_config_settings_from_value(&started_document).unwrap();
assert_eq!(effective.auth_dir, DEFAULT_AUTH_DIR);
assert_eq!(
auth_dir_path_for_core(&effective.auth_dir, &install_dir),
auth_dir_path_for_core(&effective.auth_dir, &install_dir).unwrap(),
persistent_auth_dir
);
assert_eq!(fs::read(&credential_path).unwrap(), b"existing-credential");
@@ -401,9 +560,9 @@ fn macos_oauth_recovery_survives_config_reload_and_v8_upgrade() {
}
#[test]
fn macos_oauth_recovery_preserves_config_when_inspection_or_patch_fails() {
fn oauth_recovery_preserves_config_when_inspection_or_patch_fails() {
for failure in ["invalid-yaml", "unreadable-config", "invalid-auth-directory"] {
let root = agent_test_home("macos-oauth-recovery-failure");
let root = agent_test_home("oauth-recovery-failure");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
let core_config_path = install_dir.join(CORE_CONFIG_FILE);
@@ -426,7 +585,12 @@ fn macos_oauth_recovery_preserves_config_when_inspection_or_patch_fails() {
let original_config = toml::to_string(&config).unwrap();
let original_yaml = fs::read(&core_config_path).ok();
assert!(migrate_macos_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).is_err());
assert!(migrate_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).is_err());
assert_eq!(toml::to_string(&config).unwrap(), original_config);
assert_eq!(fs::read(&core_config_path).ok(), original_yaml);
assert!(
!sanitize_gui_config_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(toml::to_string(&config).unwrap(), original_config);
assert_eq!(fs::read(&core_config_path).ok(), original_yaml);
assert_eq!(
@@ -438,8 +602,8 @@ fn macos_oauth_recovery_preserves_config_when_inspection_or_patch_fails() {
}
#[test]
fn macos_oauth_recovery_requires_existing_persistent_credentials() {
let root = agent_test_home("macos-oauth-recovery-empty");
fn oauth_recovery_requires_existing_persistent_credentials() {
let root = agent_test_home("oauth-recovery-empty");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
let mut config = GuiConfigFile {
@@ -447,13 +611,13 @@ fn macos_oauth_recovery_requires_existing_persistent_credentials() {
..GuiConfigFile::default()
};
assert!(
!migrate_macos_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
!migrate_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
fs::create_dir_all(persistent_auth_dir.join("nested")).unwrap();
fs::write(persistent_auth_dir.join("core.log"), b"log").unwrap();
fs::write(persistent_auth_dir.join("nested").join("account.json"), b"nested").unwrap();
assert!(
!migrate_macos_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
!migrate_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, OAUTH_DIR_NAME);
fs::remove_dir_all(root).unwrap();
@@ -461,8 +625,8 @@ fn macos_oauth_recovery_requires_existing_persistent_credentials() {
#[cfg(unix)]
#[test]
fn macos_oauth_recovery_preserves_active_linked_credentials() {
let root = agent_test_home("macos-oauth-recovery-symlink");
fn oauth_recovery_preserves_active_linked_credentials() {
let root = agent_test_home("oauth-recovery-symlink");
let install_dir = root.join("cpa-core");
let active_auth_dir = install_dir.join(OAUTH_DIR_NAME);
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
@@ -476,7 +640,7 @@ fn macos_oauth_recovery_preserves_active_linked_credentials() {
..GuiConfigFile::default()
};
assert!(
!migrate_macos_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
!migrate_auth_dir_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, OAUTH_DIR_NAME);
assert_eq!(fs::read(active_auth_dir.join("linked.json")).unwrap(), b"existing-credential");
@@ -484,8 +648,8 @@ fn macos_oauth_recovery_preserves_active_linked_credentials() {
}
#[test]
fn macos_relative_oauth_dir_preserves_populated_or_custom_paths() {
let root = agent_test_home("macos-relative-oauth-dir-custom");
fn relative_oauth_dir_preserves_populated_or_custom_paths() {
let root = agent_test_home("relative-oauth-dir-custom");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
let active_auth_dir = install_dir.join(OAUTH_DIR_NAME);
@@ -499,19 +663,48 @@ fn macos_relative_oauth_dir_preserves_populated_or_custom_paths() {
};
assert!(
!recover_macos_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap()
!recover_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, OAUTH_DIR_NAME);
fs::remove_file(active_auth_dir.join("current.json")).unwrap();
config.auth_dir = "other/oauth".to_string();
assert!(
!recover_macos_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap()
!recover_relative_oauth_dir(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, "other/oauth");
fs::remove_dir_all(root).unwrap();
}
#[test]
#[cfg(not(target_os = "macos"))]
fn oauth_recovery_preserves_app_named_custom_directories_on_other_platforms() {
let root = agent_test_home("oauth-custom-app-directory");
let install_dir = root.join("cpa-core");
let persistent_auth_dir = root.join(OAUTH_DIR_NAME);
let custom_auth_dir = root.join("Custom.app").join("Contents").join(OAUTH_DIR_NAME);
fs::create_dir_all(&custom_auth_dir).unwrap();
fs::create_dir_all(&persistent_auth_dir).unwrap();
fs::write(custom_auth_dir.join("current.json"), b"current-credential").unwrap();
fs::write(persistent_auth_dir.join("old.json"), b"old-credential").unwrap();
let configured_auth_dir = path_to_string(&custom_auth_dir);
let mut config = GuiConfigFile {
auth_dir: configured_auth_dir.clone(),
..GuiConfigFile::default()
};
assert!(
!sanitize_gui_config_at(&mut config, &install_dir, &persistent_auth_dir).unwrap()
);
assert_eq!(config.auth_dir, configured_auth_dir);
assert!(!persistent_auth_dir.join("current.json").exists());
assert_eq!(
fs::read(custom_auth_dir.join("current.json")).unwrap(),
b"current-credential"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn packaged_macos_auth_directory_is_copied_before_config_is_repointed() {
let root = agent_test_home("packaged-macos-auth-migration");
+2 -2
View File
@@ -930,14 +930,14 @@ fn core_start_log_path_follows_the_managed_logs_directory() {
let install_dir = base_dir.join("cpa-core");
assert_eq!(
core_start_log_path(&install_dir, DEFAULT_AUTH_DIR),
core_start_log_path(&install_dir, DEFAULT_AUTH_DIR).unwrap(),
base_dir
.join("oauth")
.join("logs")
.join("core-start-output.log")
);
assert_eq!(
core_start_log_path(&install_dir, "custom-auth"),
core_start_log_path(&install_dir, "custom-auth").unwrap(),
install_dir
.join("custom-auth")
.join("logs")