use std::ffi::OsStr;use std::path::PathBuf;use std::sync::Arc; use ai::workspace::WorkspaceMetadata;use chrono::{Local, Utc};use cloud_object_persistence::to_cloud_object_permissions;use diesel::connection::SimpleConnection;use pathfinder_geometry::rect::RectF;use pathfinder_geometry::vector::Vector2F;use warp_core::features::FeatureFlag;use warp_graphql::scalars::time::ServerTimestamp; use super::{ app_database_file_path, database_file_path_for_current_scope, database_file_path_for_scope, decode_path, deduplicate_events, encode_path, get_all_codebase_index_metadata, read_sqlite_data, save_app_state, save_codebase_index_metadata, setup_database, start_writer,};use crate::app_state::{ AppState, CodePaneSnapShot, CodePaneTabSnapshot, LeafContents, LeafSnapshot, PaneNodeSnapshot, TabGroupSnapshot, TabSnapshot, TerminalPaneSnapshot, WindowSnapshot,};use crate::auth::UserUid;use crate::cloud_object::{CloudObjectPermissions, Owner};use crate::code::editor_management::CodeSource;use crate::notebooks::{CloudNotebook, CloudNotebookModel};use crate::persistence::model::ObjectPermissions;use crate::persistence::{ BlockCompleted, ModelEvent, PersistedDataScope, PersistenceScope, StartedCommandMetadata,};use crate::server::ids::{ClientId, ServerId};use crate::tab::SelectedTabColor;use crate::terminal::ShellLaunchData;use crate::terminal::model::block::SerializedBlock;use crate::terminal::model::session::SessionId;use crate::themes::theme::AnsiColorIdentifier;use crate::workspace::tab_group::TabGroupId;use crate::workspaces::team::{MembershipRole, Team, TeamMember};use crate::workspaces::user_profiles::UserProfileWithUID;use crate::workspaces::workspace::Workspace; #[test]fn app_scope_database_path_matches_app_database_path() { assert_eq!( database_file_path_for_scope(&PersistenceScope::App), app_database_file_path() );} #[test]fn tui_scope_database_path_is_tui_subdirectory_of_app_database_dir() { let tui_path = database_file_path_for_scope(&PersistenceScope::Tui); let app_path = database_file_path_for_scope(&PersistenceScope::App); assert_ne!(tui_path, app_path); assert_eq!( tui_path, warp_core::paths::tui_state_dir().join("warp.sqlite") ); // The TUI database lives in a `tui` subdirectory of the same base // directory that holds the GUI database, so the two front-ends never // share (or migrate) each other's database. let tui_dir = tui_path .parent() .expect("TUI database path should have a parent"); assert_eq!(tui_dir.file_name(), Some(OsStr::new("tui"))); assert_eq!(tui_dir.parent(), app_path.parent());} #[test]fn database_path_for_current_scope_defaults_to_app_scope() { // Unit tests never call `persistence::initialize`, so the process-wide // scope defaults to `App` and ad-hoc read-only connections resolve to // the GUI database. (nextest runs each test in its own process, so no // other test can have set the scope.) assert_eq!( database_file_path_for_current_scope(), app_database_file_path() );} #[test]fn remote_server_daemon_scope_database_path_uses_identity_data_dir() { let path = database_file_path_for_scope(&PersistenceScope::RemoteServerDaemon { identity_key: "user@example.com/ssh host".to_string(), }); let expected_data_dir = remote_server::setup::remote_server_daemon_data_dir("user@example.com/ssh host"); assert!(path.is_absolute()); assert_eq!( path, PathBuf::from(shellexpand::tilde(&expected_data_dir).into_owned()).join("warp.sqlite") );} #[test]fn remote_server_daemon_scope_database_path_handles_empty_identity_key() { let path = database_file_path_for_scope(&PersistenceScope::RemoteServerDaemon { identity_key: String::new(), }); let expected_data_dir = remote_server::setup::remote_server_daemon_data_dir(""); assert_eq!( path, PathBuf::from(shellexpand::tilde(&expected_data_dir).into_owned()).join("warp.sqlite") );} #[cfg(unix)]#[test]fn remote_server_daemon_database_permissions_are_owner_only() { use std::fs::Permissions; use std::os::unix::fs::{MetadataExt, PermissionsExt}; let tempdir = tempfile::tempdir().expect("tempdir should be created"); let daemon_dir = tempdir.path().join("daemon"); let database_path = daemon_dir.join("warp.sqlite"); std::fs::create_dir_all(&daemon_dir).expect("daemon dir should be created"); std::fs::set_permissions(&daemon_dir, Permissions::from_mode(0o755)) .expect("daemon dir permissions should be set"); std::fs::write(&database_path, b"").expect("database file should be created"); std::fs::set_permissions(&database_path, Permissions::from_mode(0o644)) .expect("database file permissions should be set"); super::ensure_owner_only_dir(&daemon_dir).expect("daemon dir should be owner-only"); super::ensure_owner_only_file(&database_path).expect("database file should be owner-only"); assert_eq!(daemon_dir.metadata().unwrap().mode() & 0o777, 0o700); assert_eq!(database_path.metadata().unwrap().mode() & 0o777, 0o600);} fn test_codebase_metadata(path: &str) -> WorkspaceMetadata { WorkspaceMetadata { path: PathBuf::from(path), navigated_ts: Some(Utc::now()), modified_ts: None, queried_ts: None, }} #[test]fn sqlite_read_restores_app_state_and_codebase_metadata() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let app_state = AppState { windows: vec![test_terminal_window_snapshot(false)], active_window_index: Some(0), block_lists: Default::default(), running_mcp_servers: Default::default(), }; save_app_state(&mut conn, &app_state).expect("app state should save"); let metadata = test_codebase_metadata("/tmp/remote-repo"); save_codebase_index_metadata(&mut conn, metadata.clone()) .expect("codebase index metadata should save"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::Full) .expect("persisted data should load"); let restored_app_state = restored .app_state .expect("app state should be present for the full scope"); assert_eq!(restored_app_state.windows.len(), 1); assert_eq!(restored.codebase_indices.len(), 1); assert_eq!(restored.codebase_indices[0].path, metadata.path);} /// Mirrors `init_db(&PersistenceScope::Tui)` in an isolated tempdir: the TUI/// database lives in a `tui/` subdirectory, runs the same migrations, and/// round-trips a write+read using the TUI's `PersistedDataScope`.#[test]fn tui_database_in_tui_subdirectory_round_trips_data() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("tui").join("warp.sqlite"); std::fs::create_dir_all( database_path .parent() .expect("database path should have a parent"), ) .expect("tui subdirectory should be created"); let mut conn = setup_database(&database_path).expect("database should initialize"); let metadata = test_codebase_metadata("/tmp/tui-repo"); save_codebase_index_metadata(&mut conn, metadata.clone()) .expect("codebase index metadata should save"); let writer = start_writer(conn, database_path.clone()).expect("writer should start"); writer .sender .send(ModelEvent::InsertCommand { metadata: StartedCommandMetadata { command: "ls".to_owned(), start_ts: Some(Local::now()), pwd: Some("/tmp/tui-repo".to_owned()), shell: Some("zsh".to_owned()), username: Some("test-user".to_owned()), hostname: Some("test-host".to_owned()), session_id: Some(SessionId::from(1)), git_branch: None, cloud_workflow_id: None, workflow_command: None, is_agent_executed: false, }, }) .expect("insert command event should send"); writer .sender .send(ModelEvent::UpsertUserProfiles { profiles: vec![UserProfileWithUID { firebase_uid: UserUid::new("creator-uid"), display_name: Some("MCP Creator".to_owned()), email: "creator@example.com".to_owned(), photo_url: String::new(), }], }) .expect("user profile event should send"); writer .sender .send(ModelEvent::Terminate) .expect("terminate event should send"); writer.handle.join().expect("writer should terminate"); let mut conn = setup_database(&database_path).expect("database should reopen"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::TuiFrontend) .expect("persisted data should load"); // The TUI data scope skips GUI session restoration... assert!(restored.app_state.is_none()); // ...but restores command history and shared data like creator profiles and // codebase index metadata. assert_eq!(restored.command_history.len(), 1); assert_eq!(restored.command_history[0].command, "ls"); assert_eq!(restored.user_profiles.len(), 1); assert_eq!( restored.user_profiles[0].display_name.as_deref(), Some("MCP Creator") ); assert_eq!(restored.codebase_indices.len(), 1); assert_eq!(restored.codebase_indices[0].path, metadata.path);} #[test]fn sqlite_writer_reuses_codebase_index_metadata_events() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let conn = setup_database(&database_path).expect("database should initialize"); let writer = start_writer(conn, database_path.clone()).expect("writer should start"); let metadata = test_codebase_metadata("/tmp/writer-repo"); writer .sender .send(ModelEvent::UpsertCodebaseIndexMetadata { index_metadata: Box::new(metadata.clone()), }) .expect("upsert event should send"); writer .sender .send(ModelEvent::Terminate) .expect("terminate event should send"); writer.handle.join().expect("writer should terminate"); let mut conn = setup_database(&database_path).expect("database should reopen"); let restored = get_all_codebase_index_metadata(&mut conn).expect("metadata should load"); assert_eq!(restored.len(), 1); assert_eq!(restored[0].path, metadata.path); let writer = start_writer(conn, database_path.clone()).expect("writer should restart"); writer .sender .send(ModelEvent::DeleteCodebaseIndexMetadata { repo_path: metadata.path, }) .expect("delete event should send"); writer .sender .send(ModelEvent::Terminate) .expect("terminate event should send"); writer.handle.join().expect("writer should terminate"); let mut conn = setup_database(&database_path).expect("database should reopen"); let restored = get_all_codebase_index_metadata(&mut conn).expect("metadata should load"); assert!(restored.is_empty());}#[test]fn test_deduplicate_snapshots() { let local_notebook = CloudNotebook::new_local( CloudNotebookModel { title: "Hello".to_string(), data: "World".to_string(), ai_document_id: None, conversation_id: None, }, Owner::mock_current_user(), None, ClientId::new(), ); let completed_block_1 = BlockCompleted { pane_id: vec![1, 2, 3], block: Arc::new(SerializedBlock::default()), is_local: true, }; let completed_block_2 = BlockCompleted { pane_id: vec![4, 5, 6], block: Arc::new(SerializedBlock::default()), is_local: true, }; let snapshot_1 = AppState { active_window_index: Some(1), block_lists: Default::default(), windows: Default::default(), running_mcp_servers: Default::default(), }; let snapshot_2 = AppState { active_window_index: Some(2), block_lists: Default::default(), windows: Default::default(), running_mcp_servers: Default::default(), }; let snapshot_3 = AppState { active_window_index: Some(3), block_lists: Default::default(), windows: Default::default(), running_mcp_servers: Default::default(), }; let original_events = vec![ ModelEvent::UpsertNotebook { notebook: local_notebook.clone(), }, ModelEvent::Snapshot(snapshot_1.clone()), ModelEvent::SaveBlock(completed_block_1.clone()), ModelEvent::Snapshot(snapshot_2.clone()), ModelEvent::SaveBlock(completed_block_2.clone()), ModelEvent::Snapshot(snapshot_3.clone()), ModelEvent::UpsertNotebook { notebook: local_notebook.clone(), }, ]; let filtered_events = deduplicate_events(original_events); assert_eq!(filtered_events.len(), 5); assert!(matches!( &filtered_events[0], &ModelEvent::UpsertNotebook { .. } )); // The first snapshot should have been filtered out. assert!(matches!(&filtered_events[1], &ModelEvent::SaveBlock(_))); // The second snapshot should have been filtered out. assert!(matches!(&filtered_events[2], &ModelEvent::SaveBlock(_))); // The third snapshot should be preserved. match &filtered_events[3] { ModelEvent::Snapshot(snapshot) => assert_eq!(snapshot, &snapshot_3), other => panic!("Expected ModelEvent::Snapshot, got {other:?}"), } assert!(matches!( &filtered_events[4], &ModelEvent::UpsertNotebook { .. } ));} #[test]fn test_deduplicate_no_snapshots() { let original_events = vec![ModelEvent::SaveBlock(BlockCompleted { pane_id: vec![1, 2, 3], block: Default::default(), is_local: true, })]; let filtered_events = deduplicate_events(original_events); assert_eq!(filtered_events.len(), 1); assert!(matches!(&filtered_events[0], &ModelEvent::SaveBlock(_)));} fn test_terminal_window_snapshot(vertical_tabs_panel_open: bool) -> WindowSnapshot { WindowSnapshot { tabs: vec![TabSnapshot { custom_title: None, root: PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused: true, custom_vertical_tabs_title: None, contents: LeafContents::Terminal(TerminalPaneSnapshot { uuid: vec![u8::from(vertical_tabs_panel_open) + 1], cwd: Some("/tmp".to_string()), shell_launch_data: Some(ShellLaunchData::Executable { executable_path: PathBuf::from("/bin/zsh"), shell_type: crate::terminal::shell::ShellType::Zsh, }), is_active: true, is_read_only: false, input_config: None, llm_model_override: None, active_profile_id: None, conversation_ids_to_restore: vec![], active_conversation_id: None, }), }), default_directory_color: None, selected_color: SelectedTabColor::default(), left_panel: None, right_panel: None, group_id: None, pinned: false, }], active_tab_index: 0, team_uid: None, bounds: None, fullscreen_state: Default::default(), quake_mode: false, universal_search_width: None, warp_ai_width: None, voltron_width: None, warp_drive_index_width: None, left_panel_open: false, vertical_tabs_panel_open, left_panel_width: None, right_panel_width: None, agent_management_filters: None, tab_groups: vec![], }} #[test]fn test_sqlite_round_trips_vertical_tabs_panel_open() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let app_state = AppState { windows: vec![ test_terminal_window_snapshot(false), test_terminal_window_snapshot(true), ], active_window_index: Some(1), block_lists: Default::default(), running_mcp_servers: Default::default(), }; save_app_state(&mut conn, &app_state).expect("app state should save"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::Full) .expect("app state should load") .app_state .expect("app state should be present for the full scope"); assert_eq!(restored.active_window_index, Some(1)); assert_eq!( restored .windows .iter() .map(|window| window.vertical_tabs_panel_open) .collect::<Vec<_>>(), vec![false, true] );} #[test]fn test_sqlite_round_trips_window_team_uid() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let team_uid = ServerId::from(123); let mut assigned_window = test_terminal_window_snapshot(false); assigned_window.team_uid = Some(team_uid); let app_state = AppState { windows: vec![assigned_window, test_terminal_window_snapshot(true)], active_window_index: Some(0), block_lists: Default::default(), running_mcp_servers: Default::default(), }; save_app_state(&mut conn, &app_state).expect("app state should save"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::Full) .expect("app state should load") .app_state .expect("app state should be present for the full scope"); assert_eq!(restored.windows[0].team_uid, Some(team_uid)); assert_eq!(restored.windows[1].team_uid, None);} #[test]fn test_sqlite_round_trips_custom_vertical_tabs_title() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let app_state = AppState { windows: vec![WindowSnapshot { tabs: vec![TabSnapshot { custom_title: None, root: PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused: true, custom_vertical_tabs_title: Some("Production API".to_string()), contents: LeafContents::Terminal(TerminalPaneSnapshot { uuid: vec![42], cwd: Some("/tmp".to_string()),Lines 1–500 of 1161. Load more
use std::ffi::OsStr; use std::path::PathBuf; use std::sync::Arc; use ai::workspace::WorkspaceMetadata; use chrono::{Local, Utc}; use cloud_object_persistence::to_cloud_object_permissions; use diesel::connection::SimpleConnection; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2F; use warp_core::features::FeatureFlag; use warp_graphql::scalars::time::ServerTimestamp; use super::{ app_database_file_path, database_file_path_for_current_scope, database_file_path_for_scope, decode_path, deduplicate_events, encode_path, get_all_codebase_index_metadata, read_sqlite_data, save_app_state, save_codebase_index_metadata, setup_database, start_writer, }; use crate::app_state::{ AppState, CodePaneSnapShot, CodePaneTabSnapshot, LeafContents, LeafSnapshot, PaneNodeSnapshot, TabGroupSnapshot, TabSnapshot, TerminalPaneSnapshot, WindowSnapshot, }; use crate::auth::UserUid; use crate::cloud_object::{CloudObjectPermissions, Owner}; use crate::code::editor_management::CodeSource; use crate::notebooks::{CloudNotebook, CloudNotebookModel}; use crate::persistence::model::ObjectPermissions; use crate::persistence::{ BlockCompleted, ModelEvent, PersistedDataScope, PersistenceScope, StartedCommandMetadata, }; use crate::server::ids::{ClientId, ServerId}; use crate::tab::SelectedTabColor; use crate::terminal::ShellLaunchData; use crate::terminal::model::block::SerializedBlock; use crate::terminal::model::session::SessionId; use crate::themes::theme::AnsiColorIdentifier; use crate::workspace::tab_group::TabGroupId; use crate::workspaces::team::{MembershipRole, Team, TeamMember}; use crate::workspaces::user_profiles::UserProfileWithUID; use crate::workspaces::workspace::Workspace; #[test] fn app_scope_database_path_matches_app_database_path() { assert_eq!( database_file_path_for_scope(&PersistenceScope::App), app_database_file_path() ); } #[test] fn tui_scope_database_path_is_tui_subdirectory_of_app_database_dir() { let tui_path = database_file_path_for_scope(&PersistenceScope::Tui); let app_path = database_file_path_for_scope(&PersistenceScope::App); assert_ne!(tui_path, app_path); assert_eq!( tui_path, warp_core::paths::tui_state_dir().join("warp.sqlite") ); // The TUI database lives in a `tui` subdirectory of the same base // directory that holds the GUI database, so the two front-ends never // share (or migrate) each other's database. let tui_dir = tui_path .parent() .expect("TUI database path should have a parent"); assert_eq!(tui_dir.file_name(), Some(OsStr::new("tui"))); assert_eq!(tui_dir.parent(), app_path.parent()); } #[test] fn database_path_for_current_scope_defaults_to_app_scope() { // Unit tests never call `persistence::initialize`, so the process-wide // scope defaults to `App` and ad-hoc read-only connections resolve to // the GUI database. (nextest runs each test in its own process, so no // other test can have set the scope.) assert_eq!( database_file_path_for_current_scope(), app_database_file_path() ); } #[test] fn remote_server_daemon_scope_database_path_uses_identity_data_dir() { let path = database_file_path_for_scope(&PersistenceScope::RemoteServerDaemon { identity_key: "user@example.com/ssh host".to_string(), }); let expected_data_dir = remote_server::setup::remote_server_daemon_data_dir("user@example.com/ssh host"); assert!(path.is_absolute()); assert_eq!( path, PathBuf::from(shellexpand::tilde(&expected_data_dir).into_owned()).join("warp.sqlite") ); } #[test] fn remote_server_daemon_scope_database_path_handles_empty_identity_key() { let path = database_file_path_for_scope(&PersistenceScope::RemoteServerDaemon { identity_key: String::new(), }); let expected_data_dir = remote_server::setup::remote_server_daemon_data_dir(""); assert_eq!( path, PathBuf::from(shellexpand::tilde(&expected_data_dir).into_owned()).join("warp.sqlite") ); } #[cfg(unix)] #[test] fn remote_server_daemon_database_permissions_are_owner_only() { use std::fs::Permissions; use std::os::unix::fs::{MetadataExt, PermissionsExt}; let tempdir = tempfile::tempdir().expect("tempdir should be created"); let daemon_dir = tempdir.path().join("daemon"); let database_path = daemon_dir.join("warp.sqlite"); std::fs::create_dir_all(&daemon_dir).expect("daemon dir should be created"); std::fs::set_permissions(&daemon_dir, Permissions::from_mode(0o755)) .expect("daemon dir permissions should be set"); std::fs::write(&database_path, b"").expect("database file should be created"); std::fs::set_permissions(&database_path, Permissions::from_mode(0o644)) .expect("database file permissions should be set"); super::ensure_owner_only_dir(&daemon_dir).expect("daemon dir should be owner-only"); super::ensure_owner_only_file(&database_path).expect("database file should be owner-only"); assert_eq!(daemon_dir.metadata().unwrap().mode() & 0o777, 0o700); assert_eq!(database_path.metadata().unwrap().mode() & 0o777, 0o600); } fn test_codebase_metadata(path: &str) -> WorkspaceMetadata { WorkspaceMetadata { path: PathBuf::from(path), navigated_ts: Some(Utc::now()), modified_ts: None, queried_ts: None, } } #[test] fn sqlite_read_restores_app_state_and_codebase_metadata() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let app_state = AppState { windows: vec![test_terminal_window_snapshot(false)], active_window_index: Some(0), block_lists: Default::default(), running_mcp_servers: Default::default(), }; save_app_state(&mut conn, &app_state).expect("app state should save"); let metadata = test_codebase_metadata("/tmp/remote-repo"); save_codebase_index_metadata(&mut conn, metadata.clone()) .expect("codebase index metadata should save"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::Full) .expect("persisted data should load"); let restored_app_state = restored .app_state .expect("app state should be present for the full scope"); assert_eq!(restored_app_state.windows.len(), 1); assert_eq!(restored.codebase_indices.len(), 1); assert_eq!(restored.codebase_indices[0].path, metadata.path); } /// Mirrors `init_db(&PersistenceScope::Tui)` in an isolated tempdir: the TUI /// database lives in a `tui/` subdirectory, runs the same migrations, and /// round-trips a write+read using the TUI's `PersistedDataScope`. #[test] fn tui_database_in_tui_subdirectory_round_trips_data() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("tui").join("warp.sqlite"); std::fs::create_dir_all( database_path .parent() .expect("database path should have a parent"), ) .expect("tui subdirectory should be created"); let mut conn = setup_database(&database_path).expect("database should initialize"); let metadata = test_codebase_metadata("/tmp/tui-repo"); save_codebase_index_metadata(&mut conn, metadata.clone()) .expect("codebase index metadata should save"); let writer = start_writer(conn, database_path.clone()).expect("writer should start"); writer .sender .send(ModelEvent::InsertCommand { metadata: StartedCommandMetadata { command: "ls".to_owned(), start_ts: Some(Local::now()), pwd: Some("/tmp/tui-repo".to_owned()), shell: Some("zsh".to_owned()), username: Some("test-user".to_owned()), hostname: Some("test-host".to_owned()), session_id: Some(SessionId::from(1)), git_branch: None, cloud_workflow_id: None, workflow_command: None, is_agent_executed: false, }, }) .expect("insert command event should send"); writer .sender .send(ModelEvent::UpsertUserProfiles { profiles: vec![UserProfileWithUID { firebase_uid: UserUid::new("creator-uid"), display_name: Some("MCP Creator".to_owned()), email: "creator@example.com".to_owned(), photo_url: String::new(), }], }) .expect("user profile event should send"); writer .sender .send(ModelEvent::Terminate) .expect("terminate event should send"); writer.handle.join().expect("writer should terminate"); let mut conn = setup_database(&database_path).expect("database should reopen"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::TuiFrontend) .expect("persisted data should load"); // The TUI data scope skips GUI session restoration... assert!(restored.app_state.is_none()); // ...but restores command history and shared data like creator profiles and // codebase index metadata. assert_eq!(restored.command_history.len(), 1); assert_eq!(restored.command_history[0].command, "ls"); assert_eq!(restored.user_profiles.len(), 1); assert_eq!( restored.user_profiles[0].display_name.as_deref(), Some("MCP Creator") ); assert_eq!(restored.codebase_indices.len(), 1); assert_eq!(restored.codebase_indices[0].path, metadata.path); } #[test] fn sqlite_writer_reuses_codebase_index_metadata_events() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let conn = setup_database(&database_path).expect("database should initialize"); let writer = start_writer(conn, database_path.clone()).expect("writer should start"); let metadata = test_codebase_metadata("/tmp/writer-repo"); writer .sender .send(ModelEvent::UpsertCodebaseIndexMetadata { index_metadata: Box::new(metadata.clone()), }) .expect("upsert event should send"); writer .sender .send(ModelEvent::Terminate) .expect("terminate event should send"); writer.handle.join().expect("writer should terminate"); let mut conn = setup_database(&database_path).expect("database should reopen"); let restored = get_all_codebase_index_metadata(&mut conn).expect("metadata should load"); assert_eq!(restored.len(), 1); assert_eq!(restored[0].path, metadata.path); let writer = start_writer(conn, database_path.clone()).expect("writer should restart"); writer .sender .send(ModelEvent::DeleteCodebaseIndexMetadata { repo_path: metadata.path, }) .expect("delete event should send"); writer .sender .send(ModelEvent::Terminate) .expect("terminate event should send"); writer.handle.join().expect("writer should terminate"); let mut conn = setup_database(&database_path).expect("database should reopen"); let restored = get_all_codebase_index_metadata(&mut conn).expect("metadata should load"); assert!(restored.is_empty()); } #[test] fn test_deduplicate_snapshots() { let local_notebook = CloudNotebook::new_local( CloudNotebookModel { title: "Hello".to_string(), data: "World".to_string(), ai_document_id: None, conversation_id: None, }, Owner::mock_current_user(), None, ClientId::new(), ); let completed_block_1 = BlockCompleted { pane_id: vec![1, 2, 3], block: Arc::new(SerializedBlock::default()), is_local: true, }; let completed_block_2 = BlockCompleted { pane_id: vec![4, 5, 6], block: Arc::new(SerializedBlock::default()), is_local: true, }; let snapshot_1 = AppState { active_window_index: Some(1), block_lists: Default::default(), windows: Default::default(), running_mcp_servers: Default::default(), }; let snapshot_2 = AppState { active_window_index: Some(2), block_lists: Default::default(), windows: Default::default(), running_mcp_servers: Default::default(), }; let snapshot_3 = AppState { active_window_index: Some(3), block_lists: Default::default(), windows: Default::default(), running_mcp_servers: Default::default(), }; let original_events = vec![ ModelEvent::UpsertNotebook { notebook: local_notebook.clone(), }, ModelEvent::Snapshot(snapshot_1.clone()), ModelEvent::SaveBlock(completed_block_1.clone()), ModelEvent::Snapshot(snapshot_2.clone()), ModelEvent::SaveBlock(completed_block_2.clone()), ModelEvent::Snapshot(snapshot_3.clone()), ModelEvent::UpsertNotebook { notebook: local_notebook.clone(), }, ]; let filtered_events = deduplicate_events(original_events); assert_eq!(filtered_events.len(), 5); assert!(matches!( &filtered_events[0], &ModelEvent::UpsertNotebook { .. } )); // The first snapshot should have been filtered out. assert!(matches!(&filtered_events[1], &ModelEvent::SaveBlock(_))); // The second snapshot should have been filtered out. assert!(matches!(&filtered_events[2], &ModelEvent::SaveBlock(_))); // The third snapshot should be preserved. match &filtered_events[3] { ModelEvent::Snapshot(snapshot) => assert_eq!(snapshot, &snapshot_3), other => panic!("Expected ModelEvent::Snapshot, got {other:?}"), } assert!(matches!( &filtered_events[4], &ModelEvent::UpsertNotebook { .. } )); } #[test] fn test_deduplicate_no_snapshots() { let original_events = vec![ModelEvent::SaveBlock(BlockCompleted { pane_id: vec![1, 2, 3], block: Default::default(), is_local: true, })]; let filtered_events = deduplicate_events(original_events); assert_eq!(filtered_events.len(), 1); assert!(matches!(&filtered_events[0], &ModelEvent::SaveBlock(_))); } fn test_terminal_window_snapshot(vertical_tabs_panel_open: bool) -> WindowSnapshot { WindowSnapshot { tabs: vec![TabSnapshot { custom_title: None, root: PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused: true, custom_vertical_tabs_title: None, contents: LeafContents::Terminal(TerminalPaneSnapshot { uuid: vec![u8::from(vertical_tabs_panel_open) + 1], cwd: Some("/tmp".to_string()), shell_launch_data: Some(ShellLaunchData::Executable { executable_path: PathBuf::from("/bin/zsh"), shell_type: crate::terminal::shell::ShellType::Zsh, }), is_active: true, is_read_only: false, input_config: None, llm_model_override: None, active_profile_id: None, conversation_ids_to_restore: vec![], active_conversation_id: None, }), }), default_directory_color: None, selected_color: SelectedTabColor::default(), left_panel: None, right_panel: None, group_id: None, pinned: false, }], active_tab_index: 0, team_uid: None, bounds: None, fullscreen_state: Default::default(), quake_mode: false, universal_search_width: None, warp_ai_width: None, voltron_width: None, warp_drive_index_width: None, left_panel_open: false, vertical_tabs_panel_open, left_panel_width: None, right_panel_width: None, agent_management_filters: None, tab_groups: vec![], } } #[test] fn test_sqlite_round_trips_vertical_tabs_panel_open() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let app_state = AppState { windows: vec![ test_terminal_window_snapshot(false), test_terminal_window_snapshot(true), ], active_window_index: Some(1), block_lists: Default::default(), running_mcp_servers: Default::default(), }; save_app_state(&mut conn, &app_state).expect("app state should save"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::Full) .expect("app state should load") .app_state .expect("app state should be present for the full scope"); assert_eq!(restored.active_window_index, Some(1)); assert_eq!( restored .windows .iter() .map(|window| window.vertical_tabs_panel_open) .collect::<Vec<_>>(), vec![false, true] ); } #[test] fn test_sqlite_round_trips_window_team_uid() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let team_uid = ServerId::from(123); let mut assigned_window = test_terminal_window_snapshot(false); assigned_window.team_uid = Some(team_uid); let app_state = AppState { windows: vec![assigned_window, test_terminal_window_snapshot(true)], active_window_index: Some(0), block_lists: Default::default(), running_mcp_servers: Default::default(), }; save_app_state(&mut conn, &app_state).expect("app state should save"); let restored = read_sqlite_data(&mut conn, None, PersistedDataScope::Full) .expect("app state should load") .app_state .expect("app state should be present for the full scope"); assert_eq!(restored.windows[0].team_uid, Some(team_uid)); assert_eq!(restored.windows[1].team_uid, None); } #[test] fn test_sqlite_round_trips_custom_vertical_tabs_title() { let tempdir = tempfile::tempdir().expect("tempdir should be created"); let database_path = tempdir.path().join("warp.sqlite"); let mut conn = setup_database(&database_path).expect("database should initialize"); let app_state = AppState { windows: vec![WindowSnapshot { tabs: vec![TabSnapshot { custom_title: None, root: PaneNodeSnapshot::Leaf(LeafSnapshot { is_focused: true, custom_vertical_tabs_title: Some("Production API".to_string()), contents: LeafContents::Terminal(TerminalPaneSnapshot { uuid: vec![42], cwd: Some("/tmp".to_string()),