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/3] 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 5f357646639..ba20b8c1e7f 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 f18a9059c6b..95f8f69ec5c 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/3] 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 00000000000..ceff06adcad --- /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 ba20b8c1e7f..06a496f1f09 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 95f8f69ec5c..18f3e3b13c8 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 From 038d6118ee85c18b5605ad828be85f0a3cf726fe Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:13:14 +0700 Subject: [PATCH 3/3] Translate upstream Antigravity override locator tests Cover the four upstream unusable-override arguments (missing path, empty, blank, bare name) plus a directory, the no-install case, and the offline/history outcomes of an unusable override. Restore the malformed-JSON offline-history test the earlier change replaced, and document the authoritative ANTIGRAVITY_CLI_PATH behavior and its CHANGELOG line. --- CHANGELOG.md | 1 + docs/CONFIGURATION.md | 17 +++++ .../providers/antigravity/cli_resolution.rs | 64 ++++++++++++++++--- rust/src/providers/antigravity/tests.rs | 43 ++++++++++++- 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c054768f2c1..07be6fad1b8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ menu-bar layout. - DeepSeek: show reported per-model spend in the provider details while preserving the billing currency, reporting period, zero values, and incomplete-total safeguards. ### Fixed +- Antigravity: a set but unusable `ANTIGRAVITY_CLI_PATH` (empty, missing, or not a file) now turns off automatic `agy` discovery instead of falling back to another CLI that could start an interactive login during a background refresh; a running desktop app still answers first, offline history is still shown, and unsetting the variable restores discovery. - Claude: when Hide Personal Info is enabled, keep saved account rows distinguishable with stable localized `Account N` labels and matching redacted tooltips. --- diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 99e470487bd..e8a7d301a8b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -127,6 +127,23 @@ skills, and other Grok home files stay in place. `XAI_API_KEY` and `GROK_OAUTH_TOKEN` are unset for the isolated sign-in so the browser OAuth flow is used. +## Antigravity CLI path + +When neither the Antigravity desktop app nor a signed-in `agy` session is +running, the Antigravity provider can use the `agy` CLI. Win-CodexBar looks +for it on `PATH`, then at `%LOCALAPPDATA%\agy\bin\agy.exe`, then at +`%USERPROFILE%\.local\bin\agy.exe`. Set `ANTIGRAVITY_CLI_PATH` to the full +path of `agy.exe` when it is installed somewhere else. + +A set `ANTIGRAVITY_CLI_PATH` is authoritative. If it is empty or does not +point to a file, Win-CodexBar skips the CLI source and names the variable in +the provider error instead of discovering another `agy` through `PATH` or the +install directories, so a background refresh never starts a different CLI +that could ask for an interactive sign-in. A running desktop app still +answers first, and offline conversation history is still shown when it +exists. Unset the variable to restore automatic discovery. Other providers +keep their own override behavior. + ## Source mode CLI `--source` values on this port (see `codexbar usage --help`): `auto`, `web`, `cli`, `oauth`. diff --git a/rust/src/providers/antigravity/cli_resolution.rs b/rust/src/providers/antigravity/cli_resolution.rs index ceff06adcad..c1196948917 100644 --- a/rust/src/providers/antigravity/cli_resolution.rs +++ b/rust/src/providers/antigravity/cli_resolution.rs @@ -1,3 +1,10 @@ +//! `agy` executable resolution (upstream `BinaryLocator.resolveAntigravityBinary`). +//! +//! A set `ANTIGRAVITY_CLI_PATH` is authoritative: background refreshes must not +//! discover and launch another `agy` (which could start an interactive login) +//! when the explicit override is empty, missing or not a file. Only an unset +//! variable enables discovery through PATH and the known install directories. + use std::path::PathBuf; use crate::core::ProviderError; @@ -109,19 +116,38 @@ mod tests { assert_eq!(resolved, Some(override_path)); } + /// Upstream `unusable env override fails without ambient fallback` + /// (arguments: a missing path, `""`, `" "` and the bare name `agy`), plus + /// a directory as the Windows form of "not executable". Discovery (PATH and + /// install directories) must never run, so it panics here. #[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")); + let overrides = [ + temp.path().join("missing-agy.exe"), + PathBuf::from(""), + PathBuf::from(" "), + PathBuf::from("agy"), + temp.path().to_path_buf(), + ]; + + for value in overrides { + let error = resolve_agy_binary(Some(value.clone()), || { + panic!("an unusable configured override must block automatic discovery") + }) + .expect_err("unusable override should fail closed"); + + assert!( + matches!(error, ProviderError::NotInstalled(_)), + "{value:?}: {error:?}" + ); + let message = error.to_string(); + assert!(message.contains("ANTIGRAVITY_CLI_PATH is set"), "{value:?}"); + assert!( + message.contains("automatic CLI discovery is disabled"), + "{value:?}" + ); + } } #[test] @@ -140,4 +166,22 @@ mod tests { assert_eq!(resolved, Some(discovered_path)); } + + /// Upstream `minimal fallback preserves a hit or no executable`: the last + /// candidate still resolves (above); with no installed CLI the source is + /// simply missing, which is not an error. + #[test] + fn unset_override_without_an_installed_cli_resolves_to_none() { + let temp = tempfile::tempdir().expect("temporary directory"); + + let resolved = resolve_agy_binary(None, || { + vec![ + temp.path().join("missing-first.exe"), + temp.path().join("missing-last.exe"), + ] + }) + .expect("discovery without a hit is not an error"); + + assert_eq!(resolved, None); + } } diff --git a/rust/src/providers/antigravity/tests.rs b/rust/src/providers/antigravity/tests.rs index 18f3e3b13c8..5d36a4b0b3f 100644 --- a/rust/src/providers/antigravity/tests.rs +++ b/rust/src/providers/antigravity/tests.rs @@ -691,6 +691,25 @@ async fn malformed_structured_cli_json_from_fallback_is_a_parse_error() { #[tokio::test] async fn cli_fallback_error_prefers_offline_history() { + let result = AntigravityProvider::new() + .resolve_runtime_fallback_with_offline( + Err(ProviderError::AuthRequired), + || async { + Err(ProviderError::Parse( + "Antigravity CLI usage report: malformed JSON".to_string(), + )) + }, + Some(offline_result()), + ) + .await + .expect("offline history should survive a failed CLI probe"); + + assert_eq!(result.source_label, "offline"); + assert_eq!(result.usage.login_method.as_deref(), Some("offline")); +} + +#[tokio::test] +async fn unusable_cli_override_prefers_offline_history() { let result = AntigravityProvider::new() .resolve_runtime_fallback_with_offline( Err(ProviderError::AuthRequired), @@ -702,12 +721,34 @@ async fn cli_fallback_error_prefers_offline_history() { Some(offline_result()), ) .await - .expect("offline history should survive a failed CLI probe"); + .expect("offline history should survive an unusable CLI override"); assert_eq!(result.source_label, "offline"); assert_eq!(result.usage.login_method.as_deref(), Some("offline")); } +#[tokio::test] +async fn unusable_cli_override_without_history_reports_the_override() { + let result = AntigravityProvider::new() + .resolve_runtime_fallback_with_offline( + Err(ProviderError::Other("local API unavailable".to_string())), + || async { + Err(ProviderError::NotInstalled( + "ANTIGRAVITY_CLI_PATH is set but does not point to a usable agy file".into(), + )) + }, + None, + ) + .await; + + match result { + Err(ProviderError::NotInstalled(message)) => { + assert!(message.contains("ANTIGRAVITY_CLI_PATH"), "{message}"); + } + other => panic!("expected the actionable override error, got {other:?}"), + } +} + #[test] fn user_tier_resolves_google_ai_ultra_plan_name() { let json = serde_json::json!({