Files
galaxy/app/src/settings/onboarding_tests.rs
T

228 lines
10 KiB
Rust

use ai::LLMId;
use chrono::{DateTime, Utc};
use galaxy_core::features::FeatureFlag;
use galaxyui::{App, SingletonEntity};
use onboarding::slides::{AgentAutonomy, AgentDevelopmentSettings, ProjectOnboardingSettings};
use onboarding::SelectedSettings;
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::{
AIExecutionProfile, ActionPermission, CloudAIExecutionProfileModel,
};
use crate::ai::mcp::TemplatableMCPServerManager;
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
use crate::cloud_object::{Revision, ServerAIExecutionProfile, ServerMetadata, ServerPermissions};
use crate::network::NetworkStatus;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::{ServerId, SyncId};
use crate::server::sync_queue::SyncQueue;
use crate::settings::{apply_onboarding_settings, AISettings, PrivacySettings};
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::LaunchMode;
fn mock_server_metadata(uid: ServerId) -> ServerMetadata {
ServerMetadata {
uid,
revision: Revision::now(),
metadata_last_updated_ts: DateTime::<Utc>::default().into(),
trashed_ts: None,
folder_id: None,
is_welcome_object: false,
creator_uid: None,
last_editor_uid: None,
current_editor_uid: None,
}
}
/// Regression test for: "Logging in to an existing user at the end of
/// onboarding should preserve the user's cloud-stored default execution
/// profile rather than overwriting it with the onboarding-selected
/// base_model and autonomy."
///
/// Simulates the full post-login flow:
/// 1. User starts unauthenticated; `AIExecutionProfilesModel` begins in
/// `Unsynced`.
/// 2. User logs into an existing account; their cloud default profile
/// arrives via initial bulk load and `InitialLoadCompleted` fires.
/// 3. The reconciliation handler promotes the local state to `Synced`
/// with the cloud profile's `sync_id`.
/// 4. `apply_onboarding_settings` runs (as it would from
/// `handle_cloud_preferences_syncer_event`) with onboarding-selected
/// values that differ from what's on the cloud profile.
/// 5. The cloud profile's stored values must be preserved.
#[test]
fn apply_onboarding_settings_preserves_existing_cloud_profile_on_existing_user_login() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(SyncQueue::mock);
app.add_singleton_model(|_| NetworkStatus::new());
app.add_singleton_model(TeamTesterStatus::mock);
app.add_singleton_model(UpdateManager::mock);
app.add_singleton_model(CloudModel::mock);
app.add_singleton_model(|_| TemplatableMCPServerManager::default());
app.add_singleton_model(PrivacySettings::mock);
app.add_singleton_model(UserWorkspaces::default_mock);
let profile_model = app.add_singleton_model(|ctx| {
AIExecutionProfilesModel::new(&LaunchMode::new_for_unit_test(), ctx)
});
// The existing user's stored cloud default profile. Values are
// deliberately chosen to differ from both `AIExecutionProfile`'s
// defaults and from the onboarding values we'll pass below, so any
// accidental overwrite is detectable.
let cloud_uid = ServerId::from(7);
let cloud_sync_id = SyncId::ServerId(cloud_uid);
let cloud_stored_model = LLMId::from("claude-existing-cloud-model");
let cloud_profile = AIExecutionProfile {
name: "Default".to_string(),
is_default_profile: true,
base_model: Some(cloud_stored_model.clone()),
apply_code_diffs: ActionPermission::AlwaysAllow,
read_files: ActionPermission::AlwaysAllow,
execute_commands: ActionPermission::AlwaysAllow,
mcp_permissions: ActionPermission::AlwaysAllow,
..Default::default()
};
let server_object = ServerAIExecutionProfile::new(
cloud_sync_id,
CloudAIExecutionProfileModel::new(cloud_profile),
mock_server_metadata(cloud_uid),
ServerPermissions::mock_personal(),
);
// Insert the existing user's cloud profile via the initial-load
// path (no per-object events) and emit `InitialLoadCompleted` so
// `AIExecutionProfilesModel` reconciles to `Synced`.
CloudModel::handle(&app).update(&mut app, move |cloud_model, ctx| {
let server_objects: Vec<ServerAIExecutionProfile> = vec![server_object];
cloud_model.update_objects_from_initial_load(server_objects, false, false, ctx);
ctx.emit(CloudModelEvent::InitialLoadCompleted);
});
// Sanity: reconciliation occurred and the model now reads the
// cloud profile.
profile_model.read(&app, |model, ctx| {
let info = model.default_profile(ctx);
assert_eq!(info.sync_id(), Some(cloud_sync_id));
assert_eq!(info.data().base_model, Some(cloud_stored_model.clone()));
});
// Simulate the onboarding handoff: the user picked a different
// base_model and "None" autonomy on the agent slide, which would
// map to every `ActionPermission` being `AlwaysAsk`.
let onboarding_settings = SelectedSettings::AgentDrivenDevelopment {
agent_settings: AgentDevelopmentSettings {
selected_model_id: LLMId::from("onboarding-chosen-model"),
autonomy: Some(AgentAutonomy::None),
cli_agent_toolbar_enabled: true,
session_default: onboarding::SessionDefault::Agent,
disable_oz: false,
show_agent_notifications: true,
},
project_settings: ProjectOnboardingSettings::default(),
ui_customization: None,
};
app.update(|ctx| {
apply_onboarding_settings(&onboarding_settings, true, ctx);
});
// Post-condition: the cloud profile retains its stored values.
// Every field touched by `apply_agent_settings` should be
// unchanged.
profile_model.read(&app, |model, ctx| {
let info = model.default_profile(ctx);
assert_eq!(
info.sync_id(),
Some(cloud_sync_id),
"still pointing at the existing cloud profile"
);
assert_eq!(
info.data().base_model,
Some(cloud_stored_model.clone()),
"base_model should not be overwritten by onboarding for existing users"
);
assert_eq!(
info.data().apply_code_diffs,
ActionPermission::AlwaysAllow,
"apply_code_diffs should not be overwritten by onboarding for existing users"
);
assert_eq!(
info.data().read_files,
ActionPermission::AlwaysAllow,
"read_files should not be overwritten by onboarding for existing users"
);
assert_eq!(
info.data().execute_commands,
ActionPermission::AlwaysAllow,
"execute_commands should not be overwritten by onboarding for existing users"
);
assert_eq!(
info.data().mcp_permissions,
ActionPermission::AlwaysAllow,
"mcp_permissions should not be overwritten by onboarding for existing users"
);
});
})
}
/// Warp's AI features run on a Warp account. For third-party agent intent
/// (`disable_oz = true`), AI is therefore off when the user skips creating an
/// account and on once they have one.
#[test]
fn apply_onboarding_settings_gates_third_party_ai_on_account() {
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(SyncQueue::mock);
app.add_singleton_model(|_| NetworkStatus::new());
app.add_singleton_model(TeamTesterStatus::mock);
app.add_singleton_model(UpdateManager::mock);
app.add_singleton_model(CloudModel::mock);
app.add_singleton_model(|_| TemplatableMCPServerManager::default());
app.add_singleton_model(PrivacySettings::mock);
app.add_singleton_model(UserWorkspaces::default_mock);
app.add_singleton_model(|ctx| {
AIExecutionProfilesModel::new(&LaunchMode::new_for_unit_test(), ctx)
});
let onboarding_settings = SelectedSettings::AgentDrivenDevelopment {
agent_settings: AgentDevelopmentSettings {
selected_model_id: LLMId::from("auto"),
autonomy: None,
cli_agent_toolbar_enabled: true,
session_default: onboarding::SessionDefault::Agent,
disable_oz: true,
show_agent_notifications: true,
},
project_settings: ProjectOnboardingSettings::default(),
ui_customization: None,
};
// Skipping login (no account) leaves AI off, even for agent intent.
app.update(|ctx| {
apply_onboarding_settings(&onboarding_settings, false, ctx);
});
let ai_disabled = app.read(|ctx| !*AISettings::as_ref(ctx).is_any_ai_enabled);
assert!(
ai_disabled,
"skipping login must disable AI even for agent intent"
);
// Creating an account turns AI on, including for third-party agents.
app.update(|ctx| {
apply_onboarding_settings(&onboarding_settings, true, ctx);
});
let ai_enabled = app.read(|ctx| *AISettings::as_ref(ctx).is_any_ai_enabled);
assert!(
ai_enabled,
"creating an account must enable AI for third-party agent intent"
);
})
}