1161 lines · 44.2 KB · Rust
RawDownload
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.