581 lines
22 KiB
Rust
581 lines
22 KiB
Rust
use std::sync::Arc;
|
|
use std::time::{Duration, SystemTime};
|
|
|
|
use ai::api_keys::ApiKeyManager;
|
|
use galaxyui_extras::user_preferences;
|
|
use settings::{PrivatePreferences, PublicPreferences};
|
|
use warp_managed_secrets::ManagedSecretManager;
|
|
use warpui::{AddSingletonModel, App};
|
|
|
|
use super::*;
|
|
use crate::server::server_api::team::MockTeamClient;
|
|
use crate::server::server_api::workspace::MockWorkspaceClient;
|
|
use crate::server::server_api::ServerApiProvider;
|
|
use crate::workspaces::team::Team;
|
|
use crate::workspaces::workspace::{HostEnablementSetting, LlmHostSettings, Workspace};
|
|
|
|
// ── pure helpers ────────────────────────────────────────────────
|
|
|
|
const TEST_AUDIENCE: &str =
|
|
"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/warp-pool/providers/warp-provider";
|
|
const TEST_SA_EMAIL: &str = "warp-geap@test-project.iam.gserviceaccount.com";
|
|
|
|
#[test]
|
|
fn mint_binding_from_parts_trims_and_normalizes() {
|
|
let binding = geap_mint_binding_from_parts(
|
|
"user-1".into(),
|
|
Some(&format!(" {TEST_AUDIENCE} ")),
|
|
Some(&format!(" {TEST_SA_EMAIL} ")),
|
|
)
|
|
.expect("a configured audience yields a mintable binding");
|
|
assert_eq!(binding.audience, TEST_AUDIENCE);
|
|
assert_eq!(
|
|
binding.federation,
|
|
GeapFederation::ServiceAccount {
|
|
email: TEST_SA_EMAIL.to_string()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mint_binding_from_parts_requires_an_audience() {
|
|
// Missing or blank audience -> not mintable (rests at Unconfigured/Missing).
|
|
assert_eq!(
|
|
geap_mint_binding_from_parts("user-1".into(), None, Some(TEST_SA_EMAIL)),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
geap_mint_binding_from_parts("user-1".into(), Some(" "), Some(TEST_SA_EMAIL)),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mint_binding_from_parts_uses_direct_wif_without_sa() {
|
|
// A whitespace-only or missing SA email means "no impersonation"
|
|
// (DirectWif), not an SA named "".
|
|
let binding = geap_mint_binding_from_parts("user-1".into(), Some(TEST_AUDIENCE), Some(" "))
|
|
.expect("audience present");
|
|
assert_eq!(binding.federation, GeapFederation::DirectWif);
|
|
|
|
let binding = geap_mint_binding_from_parts("user-1".into(), Some(TEST_AUDIENCE), None)
|
|
.expect("audience present");
|
|
assert_eq!(binding.federation, GeapFederation::DirectWif);
|
|
}
|
|
|
|
#[test]
|
|
fn sts_expires_at_prefers_expires_in_and_falls_back_to_jwt_expiry() {
|
|
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
|
|
let jwt_expires_at = now + Duration::from_secs(900);
|
|
|
|
// STS reported a lifetime: absolute expiry is now + expires_in.
|
|
assert_eq!(
|
|
sts_expires_at(Some(3600), jwt_expires_at, now),
|
|
now + Duration::from_secs(3600)
|
|
);
|
|
// STS omitted it (allowed by RFC 8693): fall back to the JWT's own
|
|
// expiry as a conservative bound.
|
|
assert_eq!(sts_expires_at(None, jwt_expires_at, now), jwt_expires_at);
|
|
}
|
|
|
|
#[test]
|
|
fn impersonation_expiry_parses_rfc3339() {
|
|
let parsed = parse_generate_access_token_expiry("2026-06-11T15:01:23Z").unwrap();
|
|
assert!(parsed > SystemTime::UNIX_EPOCH);
|
|
}
|
|
|
|
#[test]
|
|
fn impersonation_expiry_rejects_invalid_timestamps() {
|
|
let err = parse_generate_access_token_expiry("not-a-timestamp").unwrap_err();
|
|
assert!(err.contains("invalid expireTime"));
|
|
}
|
|
|
|
#[test]
|
|
fn impersonation_response_parses_camel_case() {
|
|
let response: GenerateAccessTokenResponse =
|
|
serde_json::from_str(r#"{"accessToken":"ya29.token","expireTime":"2026-06-11T15:01:23Z"}"#)
|
|
.unwrap();
|
|
assert_eq!(response.access_token, "ya29.token");
|
|
assert_eq!(response.expire_time, "2026-06-11T15:01:23Z");
|
|
}
|
|
|
|
#[test]
|
|
fn sts_response_parses_with_and_without_expires_in() {
|
|
let with: StsTokenExchangeResponse =
|
|
serde_json::from_str(r#"{"access_token":"fed-token","expires_in":3599}"#).unwrap();
|
|
assert_eq!(with.access_token, "fed-token");
|
|
assert_eq!(with.expires_in, Some(3599));
|
|
|
|
let without: StsTokenExchangeResponse =
|
|
serde_json::from_str(r#"{"access_token":"fed-token"}"#).unwrap();
|
|
assert_eq!(without.expires_in, None);
|
|
}
|
|
|
|
#[test]
|
|
fn timer_delay_fires_lead_time_before_expiry() {
|
|
let now = SystemTime::now();
|
|
let delay = geap_refresh_timer_delay(now + Duration::from_secs(3600), now);
|
|
assert_eq!(delay, Duration::from_secs(3600) - GEAP_REFRESH_LEAD_TIME);
|
|
}
|
|
|
|
#[test]
|
|
fn timer_delay_clamps_to_floor_when_near_or_past_expiry() {
|
|
let now = SystemTime::now();
|
|
// Within the lead window: never immediate, clamped up to the floor.
|
|
assert_eq!(
|
|
geap_refresh_timer_delay(now + Duration::from_secs(30), now),
|
|
GEAP_MIN_TIMER_DELAY
|
|
);
|
|
// Already expired (e.g. a badly skewed local clock): same floor, so the
|
|
// timer cannot spin a hot mint loop.
|
|
assert_eq!(
|
|
geap_refresh_timer_delay(now - Duration::from_secs(30), now),
|
|
GEAP_MIN_TIMER_DELAY
|
|
);
|
|
}
|
|
|
|
// The structured `LoadGeapCredentialsError` is intentionally not user-facing
|
|
// (no `Display` prose, no truncation) — the UI layer owns turning a leg + HTTP
|
|
// `status` + raw `detail` into actionable copy. Coverage that the structured
|
|
// error round-trips into `Failed { error }` lives in the mint-completion tests
|
|
// below.
|
|
|
|
// ── refresh guard / safety net (app harness) ───────────────────
|
|
|
|
fn team_for_test() -> Team {
|
|
Team {
|
|
uid: 123.into(),
|
|
name: "test".to_string(),
|
|
invite_code: None,
|
|
members: vec![],
|
|
pending_email_invites: vec![],
|
|
invite_link_domain_restrictions: vec![],
|
|
billing_metadata: Default::default(),
|
|
stripe_customer_id: None,
|
|
organization_settings: Default::default(),
|
|
is_eligible_for_discovery: false,
|
|
has_billing_history: false,
|
|
}
|
|
}
|
|
|
|
fn workspace_with_geap_host(enabled: bool) -> Workspace {
|
|
let team = team_for_test();
|
|
let mut workspace = Workspace {
|
|
uid: "workspace_uid123456789".to_string().into(),
|
|
name: "test".to_string(),
|
|
stripe_customer_id: None,
|
|
teams: vec![team],
|
|
billing_metadata: Default::default(),
|
|
bonus_grants_purchased_this_month: Default::default(),
|
|
billing_cycle_usage: None,
|
|
has_billing_history: false,
|
|
settings: Default::default(),
|
|
invite_code: None,
|
|
invite_link_domain_restrictions: vec![],
|
|
pending_email_invites: vec![],
|
|
is_eligible_for_discovery: false,
|
|
members: vec![],
|
|
total_requests_used_since_last_refresh: 0,
|
|
};
|
|
workspace.settings.llm_settings.enabled = true;
|
|
workspace.settings.llm_settings.host_configs.insert(
|
|
crate::ai::llms::LLMModelHost::GeminiEnterprise,
|
|
LlmHostSettings {
|
|
enabled,
|
|
enablement_setting: HostEnablementSetting::Enforce,
|
|
gcp_audience: Some(TEST_AUDIENCE.to_string()),
|
|
gcp_sa_email: Some(TEST_SA_EMAIL.to_string()),
|
|
},
|
|
);
|
|
workspace
|
|
}
|
|
|
|
/// Registers the minimal singleton set the refresh path touches: workspace
|
|
/// policy (gate), auth (uid), settings (member toggle), the secret manager
|
|
/// (leg 1 mint), and the `ApiKeyManager` under test.
|
|
fn initialize_app(app: &mut App, workspaces: Vec<Workspace>) {
|
|
app.add_singleton_model(|_| {
|
|
PublicPreferences::new(Box::<user_preferences::in_memory::InMemoryPreferences>::default())
|
|
});
|
|
app.add_singleton_model(|_| {
|
|
PrivatePreferences::new(Box::<user_preferences::in_memory::InMemoryPreferences>::default())
|
|
});
|
|
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
|
|
let auth_state_provider = crate::auth::AuthStateProvider::new_for_test();
|
|
let auth_state = auth_state_provider.get().clone();
|
|
app.add_singleton_model(|_| auth_state_provider);
|
|
app.add_singleton_model(crate::settings::AISettings::new_with_defaults);
|
|
app.add_singleton_model(|ctx| {
|
|
UserWorkspaces::mock(
|
|
Arc::new(MockTeamClient::new()),
|
|
Arc::new(MockWorkspaceClient::new()),
|
|
workspaces,
|
|
ctx,
|
|
)
|
|
});
|
|
app.add_singleton_model(|ctx| {
|
|
ManagedSecretManager::new(
|
|
ServerApiProvider::as_ref(ctx).get_managed_secrets_client(),
|
|
auth_state,
|
|
)
|
|
});
|
|
app.update(|ctx| {
|
|
galaxyui_extras::secure_storage::register_noop("test", ctx);
|
|
ctx.add_singleton_model(ApiKeyManager::new);
|
|
});
|
|
}
|
|
|
|
fn fresh_credentials() -> GeapCredentials {
|
|
GeapCredentials::new(
|
|
"geap-token".into(),
|
|
Some(SystemTime::now() + Duration::from_secs(3600)),
|
|
)
|
|
}
|
|
|
|
fn expired_credentials() -> GeapCredentials {
|
|
GeapCredentials::new(
|
|
"geap-token".into(),
|
|
Some(SystemTime::now() - Duration::from_secs(30)),
|
|
)
|
|
}
|
|
|
|
/// A binding that does not match the harness gate (minted before an account
|
|
/// switch).
|
|
fn stale_binding() -> GeapMintBinding {
|
|
GeapMintBinding {
|
|
user_uid: "previous-user".into(),
|
|
audience: TEST_AUDIENCE.into(),
|
|
federation: GeapFederation::ServiceAccount {
|
|
email: TEST_SA_EMAIL.into(),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// The mintable binding for the harness gate. The harness enables the GEAP
|
|
/// host with a configured audience, so the policy is always `Mintable`.
|
|
fn current_binding(ctx: &mut ModelContext<ApiKeyManager>) -> GeapMintBinding {
|
|
match current_geap_policy(ctx) {
|
|
GeapPolicy::Mintable(binding) => binding,
|
|
other => panic!("expected a mintable GEAP policy, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_disables_and_drops_tokens_when_gate_is_off() {
|
|
// GEAP host present but disabled by the admin.
|
|
let workspace = workspace_with_geap_host(false);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
|
|
// Even a previously loaded token is dropped: no token is retained
|
|
// while disabled.
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Loaded {
|
|
credentials: fresh_credentials(),
|
|
loaded_at: SystemTime::now(),
|
|
minted_for: GeapMintBinding {
|
|
user_uid: "user".into(),
|
|
audience: TEST_AUDIENCE.into(),
|
|
federation: GeapFederation::ServiceAccount {
|
|
email: TEST_SA_EMAIL.into(),
|
|
},
|
|
},
|
|
},
|
|
ctx,
|
|
);
|
|
refresh_geap_credentials(manager, ctx);
|
|
assert_eq!(
|
|
*manager.geap_credentials_state(),
|
|
GeapCredentialsState::Disabled
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_rests_at_missing_when_enabled_but_unconfigured() {
|
|
let mut workspace = workspace_with_geap_host(true);
|
|
// Enabled, but the admin has not configured an audience yet.
|
|
workspace
|
|
.settings
|
|
.llm_settings
|
|
.host_configs
|
|
.get_mut(&crate::ai::llms::LLMModelHost::GeminiEnterprise)
|
|
.unwrap()
|
|
.gcp_audience = Some(" ".to_string());
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
refresh_geap_credentials(manager, ctx);
|
|
assert_eq!(
|
|
*manager.geap_credentials_state(),
|
|
GeapCredentialsState::Missing
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_skips_when_token_is_fresh_and_binding_matches() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
let loaded = GeapCredentialsState::Loaded {
|
|
credentials: fresh_credentials(),
|
|
loaded_at: SystemTime::now(),
|
|
minted_for: current_binding(ctx),
|
|
};
|
|
manager.set_geap_credentials_state(loaded.clone(), ctx);
|
|
// Skip-if-valid: a fresh token under the current binding means no
|
|
// re-mint and no state change.
|
|
refresh_geap_credentials(manager, ctx);
|
|
assert_eq!(*manager.geap_credentials_state(), loaded);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_noops_while_a_mint_is_in_flight() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
let in_flight = GeapCredentialsState::Refreshing {
|
|
previous: Some((fresh_credentials(), current_binding(ctx))),
|
|
};
|
|
manager.set_geap_credentials_state(in_flight.clone(), ctx);
|
|
// One mint at a time — force included.
|
|
refresh_geap_credentials(manager, ctx);
|
|
assert_eq!(*manager.geap_credentials_state(), in_flight);
|
|
force_refresh_geap_credentials(manager, ctx);
|
|
assert_eq!(*manager.geap_credentials_state(), in_flight);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_remints_when_token_needs_refresh() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Loaded {
|
|
credentials: expired_credentials(),
|
|
loaded_at: SystemTime::now(),
|
|
minted_for: current_binding(ctx),
|
|
},
|
|
ctx,
|
|
);
|
|
refresh_geap_credentials(manager, ctx);
|
|
// The expired-but-still-serving token rides along as `previous`
|
|
// while the re-mint is in flight: tokens stay until replaced.
|
|
match manager.geap_credentials_state() {
|
|
GeapCredentialsState::Refreshing { previous: Some(_) } => {}
|
|
other => panic!("expected Refreshing with a previous token, got {other:?}"),
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_remints_on_binding_mismatch() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
// Fresh token, but minted for a different user (e.g. before an
|
|
// account switch).
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Loaded {
|
|
credentials: fresh_credentials(),
|
|
loaded_at: SystemTime::now(),
|
|
minted_for: stale_binding(),
|
|
},
|
|
ctx,
|
|
);
|
|
refresh_geap_credentials(manager, ctx);
|
|
// The mismatched token must NOT ride along as `previous`: it is
|
|
// unservable, and restoring it on a failed re-mint would mask the
|
|
// failure behind a misleading `Loaded`.
|
|
match manager.geap_credentials_state() {
|
|
GeapCredentialsState::Refreshing { previous: None } => {}
|
|
other => panic!("expected a re-mint with no carried token, got {other:?}"),
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
// ── mint completion (apply_geap_mint_result) ─────────────────────
|
|
|
|
#[test]
|
|
fn mint_completion_discards_stale_binding_result_and_remints() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Refreshing { previous: None },
|
|
ctx,
|
|
);
|
|
// A mint stamped for a stale binding completes successfully: the
|
|
// token must be discarded — never stored — with a re-mint under
|
|
// the current binding immediately in flight (no one-request
|
|
// token-less window).
|
|
apply_geap_mint_result(
|
|
manager,
|
|
Ok(fresh_credentials()),
|
|
stale_binding(),
|
|
false,
|
|
ctx,
|
|
);
|
|
match manager.geap_credentials_state() {
|
|
GeapCredentialsState::Refreshing { previous: None } => {}
|
|
other => panic!("expected an immediate re-mint, got {other:?}"),
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn mint_completion_failure_restores_servable_previous() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
let current = current_binding(ctx);
|
|
let carried = fresh_credentials();
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Refreshing {
|
|
previous: Some((carried.clone(), current.clone())),
|
|
},
|
|
ctx,
|
|
);
|
|
// A failed background re-mint restores the still-servable
|
|
// previous token and parks the chain.
|
|
apply_geap_mint_result(
|
|
manager,
|
|
Err(LoadGeapCredentialsError::ExchangeToken {
|
|
status: None,
|
|
detail: "boom".into(),
|
|
}),
|
|
current.clone(),
|
|
false,
|
|
ctx,
|
|
);
|
|
match manager.geap_credentials_state() {
|
|
GeapCredentialsState::Loaded {
|
|
credentials,
|
|
minted_for,
|
|
..
|
|
} if *credentials == carried && *minted_for == current => {}
|
|
other => panic!("expected the previous token restored, got {other:?}"),
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn mint_completion_failure_with_unservable_previous_fails() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
let current = current_binding(ctx);
|
|
// The carried token was minted for someone else: restoring it
|
|
// would mask the failure behind a misleading `Loaded`, so the
|
|
// failure must surface instead.
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Refreshing {
|
|
previous: Some((fresh_credentials(), stale_binding())),
|
|
},
|
|
ctx,
|
|
);
|
|
apply_geap_mint_result(
|
|
manager,
|
|
Err(LoadGeapCredentialsError::ExchangeToken {
|
|
status: None,
|
|
detail: "boom".into(),
|
|
}),
|
|
current,
|
|
false,
|
|
ctx,
|
|
);
|
|
match manager.geap_credentials_state() {
|
|
GeapCredentialsState::Failed {
|
|
error: LoadGeapCredentialsError::ExchangeToken { .. },
|
|
} => {}
|
|
other => {
|
|
panic!("expected Failed carrying the structured leg-2 error, got {other:?}")
|
|
}
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn safety_net_noops_on_fresh_token_and_rearms_parked_chain() {
|
|
let workspace = workspace_with_geap_host(true);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
let fresh = GeapCredentialsState::Loaded {
|
|
credentials: fresh_credentials(),
|
|
loaded_at: SystemTime::now(),
|
|
minted_for: current_binding(ctx),
|
|
};
|
|
// Fresh token: the safety net must not touch anything.
|
|
manager.set_geap_credentials_state(fresh.clone(), ctx);
|
|
refresh_geap_credentials_if_needed(manager, ctx);
|
|
assert_eq!(*manager.geap_credentials_state(), fresh);
|
|
|
|
// Parked chain (an earlier mint failed with nothing to keep):
|
|
// the next request re-arms it.
|
|
manager.set_geap_credentials_state(
|
|
GeapCredentialsState::Failed {
|
|
error: LoadGeapCredentialsError::ExchangeToken {
|
|
status: None,
|
|
detail: "boom".into(),
|
|
},
|
|
},
|
|
ctx,
|
|
);
|
|
refresh_geap_credentials_if_needed(manager, ctx);
|
|
match manager.geap_credentials_state() {
|
|
GeapCredentialsState::Refreshing { .. } => {}
|
|
other => panic!("expected the safety net to arm a refresh, got {other:?}"),
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn safety_net_is_a_pure_noop_when_gate_is_off() {
|
|
let workspace = workspace_with_geap_host(false);
|
|
App::test((), |mut app| async move {
|
|
let _geap_flag = FeatureFlag::GeminiEnterprise.override_enabled(true);
|
|
initialize_app(&mut app, vec![workspace]);
|
|
ApiKeyManager::handle(&app).update(&mut app, |manager, ctx| {
|
|
// The request path must not mutate state when the gate is off;
|
|
// state transitions belong to the event-driven triggers.
|
|
refresh_geap_credentials_if_needed(manager, ctx);
|
|
assert_eq!(
|
|
*manager.geap_credentials_state(),
|
|
GeapCredentialsState::Missing
|
|
);
|
|
});
|
|
})
|
|
}
|