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::::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 = 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" ); }) }