From 9385f0b47557d33e22fe059ec68060d0231d8191 Mon Sep 17 00:00:00 2001 From: phil-accelbyte <225106921+phil-accelbyte@users.noreply.github.com> Date: Mon, 11 May 2026 15:37:42 +0800 Subject: [PATCH 1/2] fix: resolve namespace from active profile config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service commands declaring --namespace as required were rejected by Clap even when the active profile had a namespace set. The early resolver used for Clap arg injection only checked the flag and AGS_NAMESPACE env var, so the profile-config fallback in ExecutionContext::resolve never got a chance to run — Clap failed first. Extend resolve_namespace to also load the active profile's config, matching the resolution order already used by ExecutionContext::resolve (flag -> env -> profile config). Add five unit tests covering the new fallback plus existing precedence guarantees. --- src/invocation/commands/service/parser.rs | 6 +- src/runtime/execution.rs | 168 +++++++++++++++++++++- 2 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/invocation/commands/service/parser.rs b/src/invocation/commands/service/parser.rs index 99cbecd..b54594e 100644 --- a/src/invocation/commands/service/parser.rs +++ b/src/invocation/commands/service/parser.rs @@ -86,8 +86,10 @@ pub(super) fn parse_service_args( early_resolve_selected_method(&service_schema, internal, &positional_args, selectors)?; - let namespace_resolution = - crate::runtime::execution::resolve_namespace(flags.namespace.as_deref()); + let namespace_resolution = crate::runtime::execution::resolve_namespace( + flags.namespace.as_deref(), + flags.profile.as_deref(), + ); let effective_args = inject_namespace_if_needed( service_args, &namespace_resolution, diff --git a/src/runtime/execution.rs b/src/runtime/execution.rs index 8a716c4..258ca75 100644 --- a/src/runtime/execution.rs +++ b/src/runtime/execution.rs @@ -265,16 +265,174 @@ impl ExecutionContext { } } -/// Resolve namespace from flag or environment variable. +/// Resolve namespace from flag, environment, or the active profile's config. /// -/// Used for early Clap arg injection before full auth resolution. -/// Profile config namespace resolution happens later in `ExecutionContext::resolve()`. -pub fn resolve_namespace(namespace_flag: Option<&str>) -> Option<(String, NamespaceSource)> { +/// Used for early Clap arg injection so that commands declaring `--namespace` +/// as required can still satisfy that requirement from profile config without +/// the user re-passing the flag. +/// +/// Resolution order matches `ExecutionContext::resolve()`: `--namespace` flag, +/// then `AGS_NAMESPACE` env var, then the namespace stored in the active +/// profile. Errors loading config (corrupt or missing files) are silently +/// treated as "no value" so a broken config never blocks CLI startup; Clap +/// will then surface the missing-arg error. +pub fn resolve_namespace( + namespace_flag: Option<&str>, + profile_flag: Option<&str>, +) -> Option<(String, NamespaceSource)> { if let Some(namespace) = namespace_flag { return Some((namespace.to_string(), NamespaceSource::Flag)); } if let Ok(namespace) = std::env::var(crate::runtime::config::ENV_NAMESPACE) { return Some((namespace, NamespaceSource::Environment)); } - None + let profile = crate::runtime::config::resolve_profile_name(profile_flag).ok()?; + let profile_config = crate::runtime::config::ProfileConfig::load(&profile).ok()?; + profile_config + .namespace + .map(|namespace| (namespace, NamespaceSource::Configuration)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::config::{ENV_HOME, ENV_NAMESPACE, ENV_PROFILE, GlobalConfig, ProfileConfig}; + + struct TempEnvGuard { + key: &'static str, + original: Option, + } + + impl TempEnvGuard { + fn set(key: &'static str, value: &str) -> Self { + let original = std::env::var(key).ok(); + std::env::set_var(key, value); + Self { key, original } + } + + fn clear(key: &'static str) -> Self { + let original = std::env::var(key).ok(); + std::env::remove_var(key); + Self { key, original } + } + } + + impl Drop for TempEnvGuard { + fn drop(&mut self) { + match &self.original { + Some(val) => std::env::set_var(self.key, val), + None => std::env::remove_var(self.key), + } + } + } + + /// Profile config namespace is used when no flag or env override is present — + /// this is the bug the resolver was missing before. + #[test] + #[serial_test::serial] + fn test_resolve_namespace_falls_back_to_profile_config() { + let tmp = tempfile::tempdir().unwrap(); + let _home = TempEnvGuard::set(ENV_HOME, tmp.path().to_str().unwrap()); + let _ns = TempEnvGuard::clear(ENV_NAMESPACE); + let _profile = TempEnvGuard::clear(ENV_PROFILE); + + GlobalConfig { + active_profile: Some("default".to_string()), + ..Default::default() + } + .save() + .unwrap(); + ProfileConfig { + namespace: Some("from-config".to_string()), + ..Default::default() + } + .save("default") + .unwrap(); + + let (namespace, source) = resolve_namespace(None, None).unwrap(); + assert_eq!(namespace, "from-config"); + assert_eq!(source, NamespaceSource::Configuration); + } + + /// An explicit --namespace flag wins over a profile config value. + #[test] + #[serial_test::serial] + fn test_resolve_namespace_flag_wins_over_profile_config() { + let tmp = tempfile::tempdir().unwrap(); + let _home = TempEnvGuard::set(ENV_HOME, tmp.path().to_str().unwrap()); + let _ns = TempEnvGuard::clear(ENV_NAMESPACE); + let _profile = TempEnvGuard::clear(ENV_PROFILE); + + ProfileConfig { + namespace: Some("from-config".to_string()), + ..Default::default() + } + .save("default") + .unwrap(); + + let (namespace, source) = resolve_namespace(Some("from-flag"), None).unwrap(); + assert_eq!(namespace, "from-flag"); + assert_eq!(source, NamespaceSource::Flag); + } + + /// AGS_NAMESPACE wins over a profile config value when no flag is supplied. + #[test] + #[serial_test::serial] + fn test_resolve_namespace_env_wins_over_profile_config() { + let tmp = tempfile::tempdir().unwrap(); + let _home = TempEnvGuard::set(ENV_HOME, tmp.path().to_str().unwrap()); + let _ns = TempEnvGuard::set(ENV_NAMESPACE, "from-env"); + let _profile = TempEnvGuard::clear(ENV_PROFILE); + + ProfileConfig { + namespace: Some("from-config".to_string()), + ..Default::default() + } + .save("default") + .unwrap(); + + let (namespace, source) = resolve_namespace(None, None).unwrap(); + assert_eq!(namespace, "from-env"); + assert_eq!(source, NamespaceSource::Environment); + } + + /// --profile selects a specific profile's namespace, ignoring the active profile. + #[test] + #[serial_test::serial] + fn test_resolve_namespace_uses_explicit_profile_flag() { + let tmp = tempfile::tempdir().unwrap(); + let _home = TempEnvGuard::set(ENV_HOME, tmp.path().to_str().unwrap()); + let _ns = TempEnvGuard::clear(ENV_NAMESPACE); + let _profile = TempEnvGuard::clear(ENV_PROFILE); + + ProfileConfig { + namespace: Some("default-ns".to_string()), + ..Default::default() + } + .save("default") + .unwrap(); + ProfileConfig { + namespace: Some("staging-ns".to_string()), + ..Default::default() + } + .save("staging") + .unwrap(); + + let (namespace, source) = resolve_namespace(None, Some("staging")).unwrap(); + assert_eq!(namespace, "staging-ns"); + assert_eq!(source, NamespaceSource::Configuration); + } + + /// With no flag, no env, and no config value, the resolver yields None so Clap + /// can surface the missing-arg error naturally. + #[test] + #[serial_test::serial] + fn test_resolve_namespace_none_when_nothing_set() { + let tmp = tempfile::tempdir().unwrap(); + let _home = TempEnvGuard::set(ENV_HOME, tmp.path().to_str().unwrap()); + let _ns = TempEnvGuard::clear(ENV_NAMESPACE); + let _profile = TempEnvGuard::clear(ENV_PROFILE); + + assert!(resolve_namespace(None, None).is_none()); + } } From 61ab7893656acd06ba9a82708c070c9875734157 Mon Sep 17 00:00:00 2001 From: phil-accelbyte <225106921+phil-accelbyte@users.noreply.github.com> Date: Mon, 11 May 2026 15:41:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(test):=20satisfy=20architecture=20lints?= =?UTF-8?q?=20=E2=80=94=20add=20doc=20comments=20and=20fmt=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/runtime/execution.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/runtime/execution.rs b/src/runtime/execution.rs index 258ca75..2bd4ad1 100644 --- a/src/runtime/execution.rs +++ b/src/runtime/execution.rs @@ -296,20 +296,25 @@ pub fn resolve_namespace( #[cfg(test)] mod tests { use super::*; - use crate::runtime::config::{ENV_HOME, ENV_NAMESPACE, ENV_PROFILE, GlobalConfig, ProfileConfig}; + use crate::runtime::config::{ + GlobalConfig, ProfileConfig, ENV_HOME, ENV_NAMESPACE, ENV_PROFILE, + }; + /// RAII guard that restores an environment variable after a test mutates it. struct TempEnvGuard { key: &'static str, original: Option, } impl TempEnvGuard { + /// Set an environment variable for the lifetime of the guard. fn set(key: &'static str, value: &str) -> Self { let original = std::env::var(key).ok(); std::env::set_var(key, value); Self { key, original } } + /// Clear an environment variable for the lifetime of the guard. fn clear(key: &'static str) -> Self { let original = std::env::var(key).ok(); std::env::remove_var(key); @@ -318,6 +323,7 @@ mod tests { } impl Drop for TempEnvGuard { + /// Restore the original environment variable value when the guard is dropped. fn drop(&mut self) { match &self.original { Some(val) => std::env::set_var(self.key, val),