From 6cc2981089421cf6e509ee16525f32ce11d409e3 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:45:13 +0700 Subject: [PATCH 1/2] Honor Antigravity CLI path override --- rust/src/providers/antigravity/mod.rs | 94 ++++++++++++++++++------- rust/src/providers/antigravity/tests.rs | 76 +++++++++++++++++++- 2 files changed, 142 insertions(+), 28 deletions(-) diff --git a/rust/src/providers/antigravity/mod.rs b/rust/src/providers/antigravity/mod.rs index 5f35764663..ba20b8c1e7 100755 --- a/rust/src/providers/antigravity/mod.rs +++ b/rust/src/providers/antigravity/mod.rs @@ -481,7 +481,7 @@ impl AntigravityProvider { } async fn try_print_usage_fallback(&self) -> Result, ProviderError> { - cli_fallback::try_fetch(Self::locate_agy_binary()).await + cli_fallback::try_fetch(Self::locate_agy_binary()?).await } /// Start a short-lived, headless `agy` session when neither the Antigravity @@ -512,7 +512,7 @@ impl AntigravityProvider { Err(_) => return Err(Self::managed_agy_timeout()), } - let Some(binary) = Self::locate_agy_binary() else { + let Some(binary) = Self::locate_agy_binary()? else { return Ok(ManagedAgyOutcome::Missing); }; let probe_client = crate::core::credentialed_http_client_builder() @@ -734,26 +734,35 @@ impl AntigravityProvider { #[cfg(windows)] if allow_managed_runtime { - if cli_fallback::managed_spawn_is_csrf_gated(Self::locate_agy_binary()).await { - tracing::debug!( - "skipping managed agy readiness wait because the local server requires CSRF" - ); - } else { - match self.fetch_with_managed_agy().await { - Ok(ManagedAgyOutcome::Reused(result)) => return Ok(result), - Ok(ManagedAgyOutcome::Fetched(mut result)) => { - result.source_label = AntigravityStrategyId::Cli.as_str().to_string(); - return Ok(result); - } - Ok(ManagedAgyOutcome::Missing) => {} - Err(error) => { - if matches!(error, ProviderError::AuthRequired) { - return Err(error); + match Self::locate_agy_binary() { + Ok(binary) => { + if cli_fallback::managed_spawn_is_csrf_gated(binary).await { + tracing::debug!( + "skipping managed agy readiness wait because the local server requires CSRF" + ); + } else { + match self.fetch_with_managed_agy().await { + Ok(ManagedAgyOutcome::Reused(result)) => return Ok(result), + Ok(ManagedAgyOutcome::Fetched(mut result)) => { + result.source_label = + AntigravityStrategyId::Cli.as_str().to_string(); + return Ok(result); + } + Ok(ManagedAgyOutcome::Missing) => {} + Err(error) => { + if matches!(error, ProviderError::AuthRequired) { + return Err(error); + } + tracing::debug!(%error, "managed Antigravity CLI probe failed"); + failure = Some(error); + } } - tracing::debug!(%error, "managed Antigravity CLI probe failed"); - failure = Some(error); } } + Err(error) => { + tracing::debug!(%error, "managed Antigravity CLI resolution failed"); + failure = Some(error); + } } } @@ -782,14 +791,47 @@ impl AntigravityProvider { } } - fn locate_agy_binary() -> Option { - let candidates = Self::agy_binary_candidates( + fn validate_agy_binary_override( + explicit: Option, + ) -> Result, ProviderError> { + let Some(path) = explicit else { + return Ok(None); + }; + if path.is_file() { + Ok(Some(path)) + } else { + Err(ProviderError::NotInstalled(format!( + "ANTIGRAVITY_CLI_PATH is set but does not point to a usable agy file: {}. Fix or unset the variable; automatic CLI discovery is disabled while it is set.", + path.display() + ))) + } + } + + fn locate_agy_binary() -> Result, ProviderError> { + Self::resolve_agy_binary( std::env::var_os("ANTIGRAVITY_CLI_PATH").map(PathBuf::from), - which::which("agy").ok(), - std::env::var_os("LOCALAPPDATA").map(PathBuf::from), - dirs::home_dir(), - ); - candidates.into_iter().find(|path| path.is_file()) + || { + Self::agy_binary_candidates( + None, + which::which("agy").ok(), + std::env::var_os("LOCALAPPDATA").map(PathBuf::from), + dirs::home_dir(), + ) + }, + ) + } + + fn resolve_agy_binary( + explicit: Option, + discover: F, + ) -> Result, ProviderError> + where + F: FnOnce() -> Vec, + { + if let Some(path) = Self::validate_agy_binary_override(explicit)? { + return Ok(Some(path)); + } + Ok(discover().into_iter().find(|path| path.is_file())) } fn agy_binary_candidates( diff --git a/rust/src/providers/antigravity/tests.rs b/rust/src/providers/antigravity/tests.rs index f18a9059c6..95f8f69ec5 100644 --- a/rust/src/providers/antigravity/tests.rs +++ b/rust/src/providers/antigravity/tests.rs @@ -298,6 +298,52 @@ fn managed_agy_candidates_prefer_override_then_path_then_known_installs() { ); } +#[test] +fn usable_agy_override_is_selected_without_discovery() { + let temp = tempfile::tempdir().expect("temporary directory"); + let override_path = temp.path().join("configured-agy.exe"); + std::fs::write(&override_path, b"test executable placeholder").expect("write fixture"); + + let resolved = AntigravityProvider::resolve_agy_binary(Some(override_path.clone()), || { + panic!("a configured override must not trigger automatic discovery") + }) + .expect("usable override should resolve"); + + assert_eq!(resolved, Some(override_path)); +} + +#[test] +fn unusable_agy_override_fails_without_automatic_discovery() { + let temp = tempfile::tempdir().expect("temporary directory"); + let missing_override = temp.path().join("missing-agy.exe"); + + let error = AntigravityProvider::resolve_agy_binary(Some(missing_override), || { + panic!("an invalid configured override must block automatic discovery") + }) + .expect_err("invalid override should fail closed"); + + let message = error.to_string(); + assert!(message.contains("ANTIGRAVITY_CLI_PATH is set")); + assert!(message.contains("automatic CLI discovery is disabled")); +} + +#[test] +fn unset_agy_override_preserves_automatic_discovery() { + let temp = tempfile::tempdir().expect("temporary directory"); + let discovered_path = temp.path().join("discovered-agy.exe"); + std::fs::write(&discovered_path, b"test executable placeholder").expect("write fixture"); + + let resolved = AntigravityProvider::resolve_agy_binary(None, || { + vec![ + temp.path().join("missing-first.exe"), + discovered_path.clone(), + ] + }) + .expect("automatic discovery should resolve"); + + assert_eq!(resolved, Some(discovered_path)); +} + // ── Managed lifecycle policy (fake outcomes) ─────────────────────── // // The process lifecycle itself is covered by `crate::managed_process`; these @@ -640,6 +686,32 @@ async fn local_probe_success_does_not_run_structured_cli_fallback() { assert!(!fallback_called.load(Ordering::SeqCst)); } +#[tokio::test] +async fn local_probe_success_wins_over_an_invalid_cli_override() { + let provider = AntigravityProvider::new(); + let fallback_called = Arc::new(AtomicBool::new(false)); + let marker = Arc::clone(&fallback_called); + let local = ProviderFetchResult::new(UsageSnapshot::new(RateWindow::new(10.0)), "local"); + + let result = provider + .resolve_runtime_fallback_with_offline( + Ok(Some(local)), + move || async move { + marker.store(true, Ordering::SeqCst); + Err(ProviderError::NotInstalled( + "ANTIGRAVITY_CLI_PATH is set but unusable".to_string(), + )) + }, + Some(offline_result()), + ) + .await + .expect("successful local desktop probe must remain authoritative"); + + assert_eq!(result.source_label, "local"); + assert_eq!(result.usage.primary.used_percent, 10.0); + assert!(!fallback_called.load(Ordering::SeqCst)); +} + #[tokio::test] async fn local_auth_probe_failure_uses_structured_cli_fallback() { let result = AntigravityProvider::new() @@ -699,8 +771,8 @@ async fn cli_fallback_error_prefers_offline_history() { .resolve_runtime_fallback_with_offline( Err(ProviderError::AuthRequired), || async { - Err(ProviderError::Parse( - "Antigravity CLI usage report: malformed JSON".to_string(), + Err(ProviderError::NotInstalled( + "ANTIGRAVITY_CLI_PATH is set but does not point to a usable agy file".into(), )) }, Some(offline_result()), From 1bda0d0e88bd41b5dcf38a699afb226d30cf215d Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:16:43 +0700 Subject: [PATCH 2/2] Extract Antigravity CLI resolution --- .../providers/antigravity/cli_resolution.rs | 143 ++++++++++++++++++ rust/src/providers/antigravity/mod.rs | 77 +--------- rust/src/providers/antigravity/tests.rs | 76 ---------- 3 files changed, 147 insertions(+), 149 deletions(-) create mode 100644 rust/src/providers/antigravity/cli_resolution.rs diff --git a/rust/src/providers/antigravity/cli_resolution.rs b/rust/src/providers/antigravity/cli_resolution.rs new file mode 100644 index 0000000000..ceff06adca --- /dev/null +++ b/rust/src/providers/antigravity/cli_resolution.rs @@ -0,0 +1,143 @@ +use std::path::PathBuf; + +use crate::core::ProviderError; + +pub(super) fn locate_agy_binary() -> Result, ProviderError> { + resolve_agy_binary( + std::env::var_os("ANTIGRAVITY_CLI_PATH").map(PathBuf::from), + || { + agy_binary_candidates( + which::which("agy").ok(), + std::env::var_os("LOCALAPPDATA").map(PathBuf::from), + dirs::home_dir(), + ) + }, + ) +} + +fn validate_agy_binary_override( + explicit: Option, +) -> Result, ProviderError> { + let Some(path) = explicit else { + return Ok(None); + }; + if path.is_file() { + Ok(Some(path)) + } else { + Err(ProviderError::NotInstalled(format!( + "ANTIGRAVITY_CLI_PATH is set but does not point to a usable agy file: {}. Fix or unset the variable; automatic CLI discovery is disabled while it is set.", + path.display() + ))) + } +} + +fn resolve_agy_binary( + explicit: Option, + discover: F, +) -> Result, ProviderError> +where + F: FnOnce() -> Vec, +{ + if let Some(path) = validate_agy_binary_override(explicit)? { + return Ok(Some(path)); + } + Ok(discover().into_iter().find(|path| path.is_file())) +} + +fn agy_binary_candidates( + path_lookup: Option, + local_app_data: Option, + home: Option, +) -> Vec { + let mut candidates = Vec::new(); + if let Some(path) = path_lookup { + candidates.push(path); + } + if let Some(root) = local_app_data { + candidates.push(root.join("agy").join("bin").join("agy.exe")); + } + if let Some(root) = home { + candidates.push(root.join(".local").join("bin").join(if cfg!(windows) { + "agy.exe" + } else { + "agy" + })); + } + candidates +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidates_prefer_path_then_known_installs() { + let path_lookup = PathBuf::from(r"C:\path\agy.exe"); + let local_app_data = PathBuf::from(r"C:\Users\test\AppData\Local"); + let home = PathBuf::from(r"C:\Users\test"); + + let candidates = agy_binary_candidates( + Some(path_lookup.clone()), + Some(local_app_data.clone()), + Some(home.clone()), + ); + + assert_eq!(candidates[0], path_lookup); + assert_eq!( + candidates[1], + local_app_data.join("agy").join("bin").join("agy.exe") + ); + assert_eq!( + candidates[2], + home.join(".local") + .join("bin") + .join(if cfg!(windows) { "agy.exe" } else { "agy" }) + ); + } + + #[test] + fn usable_override_is_selected_without_discovery() { + let temp = tempfile::tempdir().expect("temporary directory"); + let override_path = temp.path().join("configured-agy.exe"); + std::fs::write(&override_path, b"test executable placeholder").expect("write fixture"); + + let resolved = resolve_agy_binary(Some(override_path.clone()), || { + panic!("a configured override must not trigger automatic discovery") + }) + .expect("usable override should resolve"); + + assert_eq!(resolved, Some(override_path)); + } + + #[test] + fn unusable_override_fails_without_automatic_discovery() { + let temp = tempfile::tempdir().expect("temporary directory"); + let missing_override = temp.path().join("missing-agy.exe"); + + let error = resolve_agy_binary(Some(missing_override), || { + panic!("an invalid configured override must block automatic discovery") + }) + .expect_err("invalid override should fail closed"); + + let message = error.to_string(); + assert!(message.contains("ANTIGRAVITY_CLI_PATH is set")); + assert!(message.contains("automatic CLI discovery is disabled")); + } + + #[test] + fn unset_override_preserves_automatic_discovery() { + let temp = tempfile::tempdir().expect("temporary directory"); + let discovered_path = temp.path().join("discovered-agy.exe"); + std::fs::write(&discovered_path, b"test executable placeholder").expect("write fixture"); + + let resolved = resolve_agy_binary(None, || { + vec![ + temp.path().join("missing-first.exe"), + discovered_path.clone(), + ] + }) + .expect("automatic discovery should resolve"); + + assert_eq!(resolved, Some(discovered_path)); + } +} diff --git a/rust/src/providers/antigravity/mod.rs b/rust/src/providers/antigravity/mod.rs index ba20b8c1e7..06a496f1f0 100755 --- a/rust/src/providers/antigravity/mod.rs +++ b/rust/src/providers/antigravity/mod.rs @@ -4,6 +4,7 @@ //! Uses Windows process detection to find CSRF token mod cli_fallback; +mod cli_resolution; mod legacy_status; mod local_proto; pub mod local_sessions; @@ -24,7 +25,6 @@ use std::ffi::OsString; use std::future::Future; #[cfg(windows)] use std::os::windows::process::CommandExt; -use std::path::PathBuf; use std::process::Command; use std::sync::LazyLock; #[cfg(windows)] @@ -481,7 +481,7 @@ impl AntigravityProvider { } async fn try_print_usage_fallback(&self) -> Result, ProviderError> { - cli_fallback::try_fetch(Self::locate_agy_binary()?).await + cli_fallback::try_fetch(cli_resolution::locate_agy_binary()?).await } /// Start a short-lived, headless `agy` session when neither the Antigravity @@ -512,7 +512,7 @@ impl AntigravityProvider { Err(_) => return Err(Self::managed_agy_timeout()), } - let Some(binary) = Self::locate_agy_binary()? else { + let Some(binary) = cli_resolution::locate_agy_binary()? else { return Ok(ManagedAgyOutcome::Missing); }; let probe_client = crate::core::credentialed_http_client_builder() @@ -734,7 +734,7 @@ impl AntigravityProvider { #[cfg(windows)] if allow_managed_runtime { - match Self::locate_agy_binary() { + match cli_resolution::locate_agy_binary() { Ok(binary) => { if cli_fallback::managed_spawn_is_csrf_gated(binary).await { tracing::debug!( @@ -791,75 +791,6 @@ impl AntigravityProvider { } } - fn validate_agy_binary_override( - explicit: Option, - ) -> Result, ProviderError> { - let Some(path) = explicit else { - return Ok(None); - }; - if path.is_file() { - Ok(Some(path)) - } else { - Err(ProviderError::NotInstalled(format!( - "ANTIGRAVITY_CLI_PATH is set but does not point to a usable agy file: {}. Fix or unset the variable; automatic CLI discovery is disabled while it is set.", - path.display() - ))) - } - } - - fn locate_agy_binary() -> Result, ProviderError> { - Self::resolve_agy_binary( - std::env::var_os("ANTIGRAVITY_CLI_PATH").map(PathBuf::from), - || { - Self::agy_binary_candidates( - None, - which::which("agy").ok(), - std::env::var_os("LOCALAPPDATA").map(PathBuf::from), - dirs::home_dir(), - ) - }, - ) - } - - fn resolve_agy_binary( - explicit: Option, - discover: F, - ) -> Result, ProviderError> - where - F: FnOnce() -> Vec, - { - if let Some(path) = Self::validate_agy_binary_override(explicit)? { - return Ok(Some(path)); - } - Ok(discover().into_iter().find(|path| path.is_file())) - } - - fn agy_binary_candidates( - explicit: Option, - path_lookup: Option, - local_app_data: Option, - home: Option, - ) -> Vec { - let mut candidates = Vec::new(); - if let Some(path) = explicit { - candidates.push(path); - } - if let Some(path) = path_lookup { - candidates.push(path); - } - if let Some(root) = local_app_data { - candidates.push(root.join("agy").join("bin").join("agy.exe")); - } - if let Some(root) = home { - candidates.push(root.join(".local").join("bin").join(if cfg!(windows) { - "agy.exe" - } else { - "agy" - })); - } - candidates - } - async fn fetch_local_payload( client: &reqwest::Client, process_info: &ProcessInfo, diff --git a/rust/src/providers/antigravity/tests.rs b/rust/src/providers/antigravity/tests.rs index 95f8f69ec5..18f3e3b13c 100644 --- a/rust/src/providers/antigravity/tests.rs +++ b/rust/src/providers/antigravity/tests.rs @@ -268,82 +268,6 @@ fn missing_cli_error_explains_runtime_state() { assert!(error.contains("agy CLI was not found")); } -#[test] -fn managed_agy_candidates_prefer_override_then_path_then_known_installs() { - let explicit = PathBuf::from(r"D:\tools\agy.exe"); - let path_lookup = PathBuf::from(r"C:\path\agy.exe"); - let local_app_data = PathBuf::from(r"C:\Users\test\AppData\Local"); - let home = PathBuf::from(r"C:\Users\test"); - - let candidates = AntigravityProvider::agy_binary_candidates( - Some(explicit.clone()), - Some(path_lookup.clone()), - Some(local_app_data.clone()), - Some(home.clone()), - ); - - assert_eq!(candidates[0], explicit); - assert_eq!(candidates[1], path_lookup); - // Build expectations with `join` so the assertions match on every host: - // on Unix `\` is an ordinary character and `join` inserts `/`. - assert_eq!( - candidates[2], - local_app_data.join("agy").join("bin").join("agy.exe") - ); - assert_eq!( - candidates[3], - home.join(".local") - .join("bin") - .join(if cfg!(windows) { "agy.exe" } else { "agy" }) - ); -} - -#[test] -fn usable_agy_override_is_selected_without_discovery() { - let temp = tempfile::tempdir().expect("temporary directory"); - let override_path = temp.path().join("configured-agy.exe"); - std::fs::write(&override_path, b"test executable placeholder").expect("write fixture"); - - let resolved = AntigravityProvider::resolve_agy_binary(Some(override_path.clone()), || { - panic!("a configured override must not trigger automatic discovery") - }) - .expect("usable override should resolve"); - - assert_eq!(resolved, Some(override_path)); -} - -#[test] -fn unusable_agy_override_fails_without_automatic_discovery() { - let temp = tempfile::tempdir().expect("temporary directory"); - let missing_override = temp.path().join("missing-agy.exe"); - - let error = AntigravityProvider::resolve_agy_binary(Some(missing_override), || { - panic!("an invalid configured override must block automatic discovery") - }) - .expect_err("invalid override should fail closed"); - - let message = error.to_string(); - assert!(message.contains("ANTIGRAVITY_CLI_PATH is set")); - assert!(message.contains("automatic CLI discovery is disabled")); -} - -#[test] -fn unset_agy_override_preserves_automatic_discovery() { - let temp = tempfile::tempdir().expect("temporary directory"); - let discovered_path = temp.path().join("discovered-agy.exe"); - std::fs::write(&discovered_path, b"test executable placeholder").expect("write fixture"); - - let resolved = AntigravityProvider::resolve_agy_binary(None, || { - vec![ - temp.path().join("missing-first.exe"), - discovered_path.clone(), - ] - }) - .expect("automatic discovery should resolve"); - - assert_eq!(resolved, Some(discovered_path)); -} - // ── Managed lifecycle policy (fake outcomes) ─────────────────────── // // The process lifecycle itself is covered by `crate::managed_process`; these