14914 lines · 519.3 KB · Rust
RawDownload
use super::*;use acp_thread::{AcpThread, PermissionOptions, StubAgentConnection};use agent::ThreadStore;use agent_ui::{    ThreadId,    terminal_thread_metadata_store::{        TerminalThreadMetadata, TerminalThreadMetadataStore, TestTerminalMetadataDbName,    },    test_support::{        active_session_id, active_thread_id, open_thread_with_connection,        open_thread_with_custom_connection, send_message,    },    thread_metadata_store::{ThreadMetadata, WorktreePaths},};use chrono::DateTime;use fs::{FakeFs, Fs};use gpui::TestAppContext;use pretty_assertions::assert_eq;use project::AgentId;use settings::SettingsStore;use std::{    path::{Path, PathBuf},    sync::Arc,};use util::{path_list::PathList, rel_path::rel_path}; fn init_test(cx: &mut TestAppContext) {    cx.update(|cx| {        let settings_store = SettingsStore::test(cx);        cx.set_global(settings_store);        // Use an isolated DB so parallel tests can't see each other's        // persisted records (e.g. created-worktree records).        cx.set_global(db::AppDatabase::test_new());        theme_settings::init(theme::LoadThemes::JustBase, cx);        editor::init(cx);        ThreadStore::init_global(cx);        ThreadMetadataStore::init_global(cx);        TerminalThreadMetadataStore::init_global(cx);        language_model::LanguageModelRegistry::test(cx);        prompt_store::init(cx);    });} #[track_caller]fn assert_active_thread(sidebar: &Sidebar, session_id: &acp::SessionId, msg: &str) {    let active = sidebar.active_entry.as_ref();    let matches = active.is_some_and(|entry| {        matches!(entry, ActiveEntry::Thread { session_id: Some(active_session_id), .. } if active_session_id == session_id)            || sidebar.contents.entries.iter().any(|list_entry| {                matches!(list_entry, ListEntry::Thread(t)                    if t.metadata.session_id.as_ref() == Some(session_id)                        && entry.matches_entry(list_entry))            })    });    assert!(        matches,        "{msg}: expected active_entry for session {session_id:?}, got {:?}",        active,    );} #[track_caller]fn is_active_session(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool {    let thread_id = sidebar        .contents        .entries        .iter()        .find_map(|entry| match entry {            ListEntry::Thread(t) if t.metadata.session_id.as_ref() == Some(session_id) => {                Some(t.metadata.thread_id)            }            _ => None,        });    match thread_id {        Some(tid) => {            matches!(&sidebar.active_entry, Some(ActiveEntry::Thread { thread_id, .. }) if *thread_id == tid)        }        // Thread not in sidebar entries — can't confirm it's active.        None => false,    }} #[track_caller]fn assert_active_draft(sidebar: &Sidebar, workspace: &Entity<Workspace>, msg: &str) {    assert!(        matches!(&sidebar.active_entry, Some(ActiveEntry::Thread { workspace: ws, .. }) if ws == workspace),        "{msg}: expected active_entry to be Draft for workspace {:?}, got {:?}",        workspace.entity_id(),        sidebar.active_entry,    );} fn has_thread_entry(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool {    sidebar        .contents        .entries        .iter()        .any(|entry| matches!(entry, ListEntry::Thread(t) if t.metadata.session_id.as_ref() == Some(session_id)))} #[track_caller]fn assert_project_header_has_threads(    sidebar: &Entity<Sidebar>,    project_name: &str,    expected_has_threads: bool,    cx: &mut gpui::VisualTestContext,) {    sidebar.read_with(cx, |sidebar, _cx| {        let has_threads = sidebar.contents.entries.iter().find_map(|entry| {            if let ListEntry::ProjectHeader {                label, has_threads, ..            } = entry                && label.as_ref() == project_name            {                Some(*has_threads)            } else {                None            }        });         assert_eq!(            has_threads,            Some(expected_has_threads),            "expected project header `{project_name}` to have has_threads={expected_has_threads}, got {has_threads:?}"        );    });} #[track_caller]fn assert_remote_project_integration_sidebar_state(    sidebar: &mut Sidebar,    main_thread_id: &acp::SessionId,    remote_thread_id: &acp::SessionId,) {    let mut project_headers = sidebar.contents.entries.iter().filter_map(|entry| {        if let ListEntry::ProjectHeader { label, .. } = entry {            Some(label.as_ref())        } else {            None        }    });     let Some(project_header) = project_headers.next() else {        panic!("expected exactly one sidebar project header named `project`, found none");    };    assert_eq!(        project_header, "project",        "expected the only sidebar project header to be `project`"    );    if let Some(unexpected_header) = project_headers.next() {        panic!(            "expected exactly one sidebar project header named `project`, found extra header `{unexpected_header}`"        );    }     let mut saw_main_thread = false;    let mut saw_remote_thread = false;    for entry in &sidebar.contents.entries {        match entry {            ListEntry::ProjectHeader { label, .. } => {                assert_eq!(                    label.as_ref(),                    "project",                    "expected the only sidebar project header to be `project`"                );            }            ListEntry::Thread(thread)                if thread.metadata.session_id.as_ref() == Some(main_thread_id) =>            {                saw_main_thread = true;            }            ListEntry::Thread(thread)                if thread.metadata.session_id.as_ref() == Some(remote_thread_id) =>            {                saw_remote_thread = true;            }            ListEntry::Thread(thread) => {                let title = thread.metadata.display_title();                panic!(                    "unexpected sidebar thread while simulating remote project integration flicker: title=`{}`",                    title                );            }            ListEntry::Terminal(terminal) => {                panic!(                    "unexpected sidebar terminal while simulating remote project integration flicker: title=`{}`",                    terminal.metadata.title                );            }        }    }     assert!(        saw_main_thread,        "expected the sidebar to keep showing `Main Thread` under `project`"    );    assert!(        saw_remote_thread,        "expected the sidebar to keep showing `Worktree Thread` under `project`"    );} async fn init_test_project(    worktree_path: &str,    cx: &mut TestAppContext,) -> Entity<project::Project> {    init_test(cx);    let fs = FakeFs::new(cx.executor());    fs.insert_tree(worktree_path, serde_json::json!({ "src": {} }))        .await;    cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));    project::Project::test(fs, [worktree_path.as_ref()], cx).await} fn setup_sidebar(    multi_workspace: &Entity<MultiWorkspace>,    cx: &mut gpui::VisualTestContext,) -> Entity<Sidebar> {    let sidebar = setup_sidebar_closed(multi_workspace, cx);    multi_workspace.update_in(cx, |mw, window, cx| {        mw.toggle_sidebar(window, cx);    });    cx.run_until_parked();    sidebar} fn setup_sidebar_closed(    multi_workspace: &Entity<MultiWorkspace>,    cx: &mut gpui::VisualTestContext,) -> Entity<Sidebar> {    let multi_workspace = multi_workspace.clone();    let sidebar =        cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx)));    multi_workspace.update(cx, |mw, cx| {        mw.register_sidebar(sidebar.clone(), cx);    });    cx.run_until_parked();    sidebar} async fn save_n_test_threads(    count: u32,    project: &Entity<project::Project>,    cx: &mut gpui::VisualTestContext,) {    for i in 0..count {        save_thread_metadata(            acp::SessionId::new(Arc::from(format!("thread-{}", i))),            Some(format!("Thread {}", i + 1).into()),            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, i).unwrap(),            None,            None,            project,            cx,        )    }    cx.run_until_parked();} async fn save_test_thread_metadata(    session_id: &acp::SessionId,    project: &Entity<project::Project>,    cx: &mut TestAppContext,) {    save_thread_metadata(        session_id.clone(),        Some("Test".into()),        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),        None,        None,        project,        cx,    )} async fn save_named_thread_metadata(    session_id: &str,    title: &str,    project: &Entity<project::Project>,    cx: &mut gpui::VisualTestContext,) {    save_thread_metadata(        acp::SessionId::new(Arc::from(session_id)),        Some(SharedString::from(title.to_string())),        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),        None,        None,        project,        cx,    );    cx.run_until_parked();} /// Seeds a pre-built [`ThreadMetadata`] into the global store so tests can/// exercise flows that resolve a thread by id.fn seed_thread_metadata(metadata: ThreadMetadata, cx: &mut TestAppContext) {    cx.update(|cx| {        ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save(metadata, cx));    });    cx.run_until_parked();} /// Spins up a fresh remote project backed by a headless server sharing/// `server_fs`, opens the given worktree path on it, and returns the/// project together with the headless entity (which the caller must keep/// alive for the duration of the test) and the `RemoteConnectionOptions`/// used for the fake server. Passing those options back into/// `reuse_opts` on a subsequent call makes the new project share the/// same `RemoteConnectionIdentity`, matching how Zed treats multiple/// projects on the same SSH host.async fn start_remote_project(    server_fs: &Arc<FakeFs>,    worktree_path: &Path,    app_state: &Arc<workspace::AppState>,    reuse_opts: Option<&remote::RemoteConnectionOptions>,    cx: &mut TestAppContext,    server_cx: &mut TestAppContext,) -> (    Entity<project::Project>,    Entity<remote_server::HeadlessProject>,    remote::RemoteConnectionOptions,) {    // Bare `_` on the guard so it's dropped immediately; holding onto it    // would deadlock `connect_mock` below since the client waits on the    // guard before completing the mock handshake.    let (opts, server_session) = match reuse_opts {        Some(existing) => {            let (session, _) = remote::RemoteClient::fake_server_with_opts(existing, cx, server_cx);            (existing.clone(), session)        }        None => {            let (opts, session, _) = remote::RemoteClient::fake_server(cx, server_cx);            (opts, session)        }    };     server_cx.update(remote_server::HeadlessProject::init);    let server_executor = server_cx.executor();    let fs = server_fs.clone();    let headless = server_cx.new(|cx| {        remote_server::HeadlessProject::new(            remote_server::HeadlessAppState {                session: server_session,                fs,                http_client: Arc::new(http_client::BlockedHttpClient),                node_runtime: node_runtime::NodeRuntime::unavailable(),                languages: Arc::new(language::LanguageRegistry::new(server_executor.clone())),                extension_host_proxy: Arc::new(extension::ExtensionHostProxy::new()),                startup_time: std::time::Instant::now(),            },            false,            cx,        )    });     let remote_client = remote::RemoteClient::connect_mock(opts.clone(), cx).await;    let project = cx.update(|cx| {        let project_client = client::Client::new(            Arc::new(clock::FakeSystemClock::new()),            http_client::FakeHttpClient::with_404_response(),            cx,        );        let user_store = cx.new(|cx| client::UserStore::new(project_client.clone(), cx));        project::Project::remote(            remote_client,            project_client,            node_runtime::NodeRuntime::unavailable(),            user_store,            app_state.languages.clone(),            app_state.fs.clone(),            false,            cx,        )    });     project        .update(cx, |project, cx| {            project.find_or_create_worktree(worktree_path, true, cx)        })        .await        .expect("should open remote worktree");    cx.run_until_parked();     (project, headless, opts)} fn save_thread_metadata(    session_id: acp::SessionId,    title: Option<SharedString>,    updated_at: DateTime<Utc>,    created_at: Option<DateTime<Utc>>,    interacted_at: Option<DateTime<Utc>>,    project: &Entity<project::Project>,    cx: &mut TestAppContext,) {    cx.update(|cx| {        let worktree_paths = project.read(cx).worktree_paths(cx);        let remote_connection = project.read(cx).remote_connection_options(cx);        let thread_id = ThreadMetadataStore::global(cx)            .read(cx)            .entries()            .find(|e| e.session_id.as_ref() == Some(&session_id))            .map(|e| e.thread_id)            .unwrap_or_else(ThreadId::new);        let metadata = ThreadMetadata {            thread_id,            session_id: Some(session_id),            agent_id: agent::ZED_AGENT_ID.clone(),            title,            title_override: None,            updated_at,            created_at,            interacted_at,            worktree_paths,            archived: false,            remote_connection,        };        ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save(metadata, cx));    });    cx.run_until_parked();} fn save_thread_metadata_with_main_paths(    session_id: &str,    title: &str,    folder_paths: PathList,    main_worktree_paths: PathList,    updated_at: DateTime<Utc>,    cx: &mut TestAppContext,) {    let session_id = acp::SessionId::new(Arc::from(session_id));    let title = SharedString::from(title.to_string());    let thread_id = cx.update(|cx| {        ThreadMetadataStore::global(cx)            .read(cx)            .entries()            .find(|e| e.session_id.as_ref() == Some(&session_id))            .map(|e| e.thread_id)            .unwrap_or_else(ThreadId::new)    });    let metadata = ThreadMetadata {        thread_id,        session_id: Some(session_id),        agent_id: agent::ZED_AGENT_ID.clone(),        title: Some(title),        title_override: None,        updated_at,        created_at: None,        interacted_at: None,        worktree_paths: WorktreePaths::from_path_lists(main_worktree_paths, folder_paths).unwrap(),        archived: false,        remote_connection: None,    };    cx.update(|cx| {        ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save(metadata, cx));    });    cx.run_until_parked();} fn save_draft_metadata_with_main_paths(    title: Option<SharedString>,    folder_paths: PathList,    main_worktree_paths: PathList,    updated_at: DateTime<Utc>,    cx: &mut TestAppContext,) -> ThreadId {    let thread_id = ThreadId::new();    let metadata = ThreadMetadata {        thread_id,        session_id: None,        agent_id: agent::ZED_AGENT_ID.clone(),        title,        title_override: None,        updated_at,        created_at: None,        interacted_at: None,        worktree_paths: WorktreePaths::from_path_lists(main_worktree_paths, folder_paths).unwrap(),        archived: false,        remote_connection: None,    };    cx.update(|cx| {        ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save(metadata, cx));    });    cx.run_until_parked();    thread_id} fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {    sidebar.update_in(cx, |_, window, cx| {        cx.focus_self(window);    });    cx.run_until_parked();} fn request_test_tool_authorization(    thread: &Entity<AcpThread>,    tool_call_id: &str,    option_id: &str,    cx: &mut gpui::VisualTestContext,) {

Lines 1–500 of 14914.