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. Load more
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, ) {