/// todo(windows)/// The tests in this file assume that server_cx is running on Windows too./// We neead to find a way to test Windows-Non-Windows interactions.use crate::headless_project::HeadlessProject;use agent::{ AgentTool, NativeAgent, NativeAgentConnection, ReadFileTool, ReadFileToolInput, SkillTool, SkillToolInput, SkillToolOutput, Templates, ThreadStore, ToolCallEventStream, ToolInput, skill_body_resolver_for_project, skills_resolver_for_project,};use client::{Client, UserStore};use clock::FakeSystemClock;use collections::{HashMap, HashSet};use language_model::{LanguageModelRegistry, LanguageModelToolResultContent};use languages::rust_lang; use editor::{ Editor, SelectionEffects, actions::{ConfirmCodeAction, ToggleCodeActions}, code_context_menus::CodeContextMenu,};use extension::ExtensionHostProxy;use fs::{FakeFs, Fs};use git::{ Oid, repository::{CommitData, GitCommitTemplate, RepoPath, Worktree as GitWorktree},};use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, VisualContext};use http_client::{BlockedHttpClient, FakeHttpClient};use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point, language_settings::{AllLanguageSettings, ConfiguredLanguageServer, LanguageSettings},};use lsp::{ CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT, LanguageServerName,};use node_runtime::NodeRuntime;use project::{ ProgressToken, Project, agent_server_store::AgentServerCommand, search::{SearchQuery, SearchResult},};use remote::RemoteClient;use rpc::proto;use serde_json::json;use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};use smol::stream::StreamExt;use std::{ path::{Path, PathBuf}, rc::Rc, str::FromStr, sync::{ Arc, atomic::{AtomicUsize, Ordering}, },};use unindent::Unindent as _;use util::{path, path_list::PathList, paths::PathMatcher, rel_path::rel_path}; #[gpui::test]async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { ".git": {}, "README.md": "# project 1", "src": { "lib.rs": "fn one() -> usize { 1 }" } }, "project2": { "README.md": "# project 2", }, }), ) .await; fs.set_index_for_repo( Path::new(path!("/code/project1/.git")), &[("src/lib.rs", "fn one() -> usize { 0 }".into())], ); let (project, _headless) = init_test(&fs, cx, server_cx).await; let (worktree, _) = project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); // The client sees the worktree's contents. cx.executor().run_until_parked(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); worktree.update(cx, |worktree, _cx| { assert_eq!( worktree.paths().collect::<Vec<_>>(), vec![ rel_path("README.md"), rel_path("src"), rel_path("src/lib.rs"), ] ); }); // The user opens a buffer in the remote worktree. The buffer's // contents are loaded from the remote filesystem. let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) }) .await .unwrap(); let diff = project .update(cx, |project, cx| { project.open_unstaged_diff(buffer.clone(), cx) }) .await .unwrap(); diff.update(cx, |diff, cx| { assert_eq!( diff.base_text_string(cx).unwrap(), "fn one() -> usize { 0 }" ); }); buffer.update(cx, |buffer, cx| { assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); let ix = buffer.text().find('1').unwrap(); buffer.edit([(ix..ix + 1, "100")], None, cx); }); // The user saves the buffer. The new contents are written to the // remote filesystem. project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await .unwrap(); assert_eq!( fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(), "fn one() -> usize { 100 }" ); // A new file is created in the remote filesystem. The user // sees the new file. fs.save( path!("/code/project1/src/main.rs").as_ref(), &"fn main() {}".into(), Default::default(), ) .await .unwrap(); cx.executor().run_until_parked(); worktree.update(cx, |worktree, _cx| { assert_eq!( worktree.paths().collect::<Vec<_>>(), vec![ rel_path("README.md"), rel_path("src"), rel_path("src/lib.rs"), rel_path("src/main.rs"), ] ); }); // A file that is currently open in a buffer is renamed. fs.rename( path!("/code/project1/src/lib.rs").as_ref(), path!("/code/project1/src/lib2.rs").as_ref(), Default::default(), ) .await .unwrap(); cx.executor().run_until_parked(); buffer.update(cx, |buffer, _| { assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs")); }); fs.set_index_for_repo( Path::new(path!("/code/project1/.git")), &[("src/lib2.rs", "fn one() -> usize { 100 }".into())], ); cx.executor().run_until_parked(); diff.update(cx, |diff, cx| { assert_eq!( diff.base_text_string(cx).unwrap(), "fn one() -> usize { 100 }" ); });} #[gpui::test]async fn test_remote_telemetry_event_forwarding( cx: &mut TestAppContext, server_cx: &mut TestAppContext,) { // This mirrors `init_test`, but retains the server-side session so the test // can drive a forwarded telemetry event over the proto channel (as // `init_telemetry_forwarding` does on a real remote server). let server_fs = FakeFs::new(server_cx.executor()); server_fs .insert_tree( path!("/code"), json!({ "project1": { "README.md": "# project 1" } }), ) .await; cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx)); server_cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx)); init_logger(); let (opts, server_session, _) = RemoteClient::fake_server(cx, server_cx); let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); let proxy = Arc::new(ExtensionHostProxy::new()); server_cx.update(HeadlessProject::init); let headless = server_cx.new(|cx| { HeadlessProject::new( crate::HeadlessAppState { session: server_session.clone(), fs: server_fs.clone(), http_client, node_runtime, languages, extension_host_proxy: proxy, startup_time: std::time::Instant::now(), }, false, cx, ) }); let ssh = RemoteClient::connect_mock(opts, cx).await; let project = build_project(ssh, cx); project .update(cx, { let headless = headless.clone(); |_, cx| cx.on_release(|_, _| drop(headless)) }) .detach(); // The remote server forwards a bare `FlexibleEvent` as JSON; mirror that // here by sending the proto message the forwarding task would send. let event_json = json!({ "event_type": "fs_watcher_poll", "event_properties": { "path": "/code/project1" }, }) .to_string(); server_session .send(proto::TelemetryEvent { project_id: proto::REMOTE_SERVER_PROJECT_ID, event_json, }) .unwrap(); cx.executor().run_until_parked(); let events = project.read_with(cx, |project, _| { project.client().telemetry().queued_events() }); assert_eq!( events.len(), 1, "the forwarded event should be reported once" ); let event = &events[0]; assert_eq!(event.event_type, "fs_watcher_poll"); // The event's original properties survive the round-trip. assert_eq!( event.event_properties.get("path"), Some(&serde_json::Value::String("/code/project1".to_string())) ); // The client stamps the remote host metadata it learned at connection time. // The mock connection reports a Linux/x86_64 host over a "mock" connection. assert_eq!( event.event_properties.get("remote"), Some(&serde_json::Value::Bool(true)) ); assert_eq!( event.event_properties.get("remote_connection_type"), Some(&serde_json::Value::String("mock".to_string())) ); assert_eq!( event.event_properties.get("remote_os_name"), Some(&serde_json::Value::String("Linux".to_string())) ); assert_eq!( event.event_properties.get("remote_architecture"), Some(&serde_json::Value::String("x86_64".to_string())) );} async fn do_search_and_assert( project: &Entity<Project>, query: &str, files_to_include: PathMatcher, match_full_paths: bool, expected_paths: &[&str], mut cx: TestAppContext,) -> Vec<Entity<Buffer>> { let query = query.to_string(); let receiver = project.update(&mut cx, |project, cx| { project.search( SearchQuery::text( query, false, true, false, files_to_include, Default::default(), match_full_paths, None, ) .unwrap(), cx, ) }); let mut buffers = Vec::new(); for expected_path in expected_paths { let buffer = loop { let response = receiver.rx.recv().await.unwrap(); match response { SearchResult::Buffer { buffer, .. } => break buffer, SearchResult::LimitReached => panic!("incorrect result"), SearchResult::WaitingForScan | SearchResult::Searching => continue, } }; buffer.update(&mut cx, |buffer, cx| { assert_eq!( buffer.file().unwrap().full_path(cx).to_string_lossy(), *expected_path ) }); buffers.push(buffer); } assert!(receiver.rx.recv().await.is_err()); buffers} #[gpui::test]async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { ".git": {}, "README.md": "# project 1", "src": { "lib.rs": "fn one() -> usize { 1 }" } }, }), ) .await; let (project, headless) = init_test(&fs, cx, server_cx).await; project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); cx.run_until_parked(); let buffers = do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await; let buffer = buffers.into_iter().next().unwrap(); // test that the headless server is tracking which buffers we have open correctly. cx.run_until_parked(); headless.update(server_cx, |headless, cx| { assert!(headless.buffer_store.read(cx).has_shared_buffers()) }); do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await; server_cx.run_until_parked(); cx.update(|_| { drop(buffer); }); cx.run_until_parked(); server_cx.run_until_parked(); headless.update(server_cx, |headless, cx| { assert!(!headless.buffer_store.read(cx).has_shared_buffers()) }); do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await;} #[gpui::test]async fn test_remote_project_search_single_cpu( cx: &mut TestAppContext, server_cx: &mut TestAppContext,) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { ".git": {}, "README.md": "# project 1", "src": { "lib.rs": "fn one() -> usize { 1 }" } }, }), ) .await; // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU). // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers, // which silently drops all search channels and produces zero results. server_cx.executor().set_num_cpus(1); let (project, _) = init_test(&fs, cx, server_cx).await; project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); cx.run_until_parked(); do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await;} #[gpui::test]async fn test_remote_project_search_inclusion( cx: &mut TestAppContext, server_cx: &mut TestAppContext,) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { "README.md": "# project 1", }, "project2": { "README.md": "# project 2", }, }), ) .await; let (project, _) = init_test(&fs, cx, server_cx).await; project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project2"), true, cx) }) .await .unwrap(); cx.run_until_parked(); Lines 1–500 of 4310. Load more
/// todo(windows) /// The tests in this file assume that server_cx is running on Windows too. /// We neead to find a way to test Windows-Non-Windows interactions. use crate::headless_project::HeadlessProject; use agent::{ AgentTool, NativeAgent, NativeAgentConnection, ReadFileTool, ReadFileToolInput, SkillTool, SkillToolInput, SkillToolOutput, Templates, ThreadStore, ToolCallEventStream, ToolInput, skill_body_resolver_for_project, skills_resolver_for_project, }; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; use language_model::{LanguageModelRegistry, LanguageModelToolResultContent}; use languages::rust_lang; use editor::{ Editor, SelectionEffects, actions::{ConfirmCodeAction, ToggleCodeActions}, code_context_menus::CodeContextMenu, }; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs}; use git::{ Oid, repository::{CommitData, GitCommitTemplate, RepoPath, Worktree as GitWorktree}, }; use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, VisualContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point, language_settings::{AllLanguageSettings, ConfiguredLanguageServer, LanguageSettings}, }; use lsp::{ CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT, LanguageServerName, }; use node_runtime::NodeRuntime; use project::{ ProgressToken, Project, agent_server_store::AgentServerCommand, search::{SearchQuery, SearchResult}, }; use remote::RemoteClient; use rpc::proto; use serde_json::json; use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; use smol::stream::StreamExt; use std::{ path::{Path, PathBuf}, rc::Rc, str::FromStr, sync::{ Arc, atomic::{AtomicUsize, Ordering}, }, }; use unindent::Unindent as _; use util::{path, path_list::PathList, paths::PathMatcher, rel_path::rel_path}; #[gpui::test] async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { ".git": {}, "README.md": "# project 1", "src": { "lib.rs": "fn one() -> usize { 1 }" } }, "project2": { "README.md": "# project 2", }, }), ) .await; fs.set_index_for_repo( Path::new(path!("/code/project1/.git")), &[("src/lib.rs", "fn one() -> usize { 0 }".into())], ); let (project, _headless) = init_test(&fs, cx, server_cx).await; let (worktree, _) = project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); // The client sees the worktree's contents. cx.executor().run_until_parked(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); worktree.update(cx, |worktree, _cx| { assert_eq!( worktree.paths().collect::<Vec<_>>(), vec![ rel_path("README.md"), rel_path("src"), rel_path("src/lib.rs"), ] ); }); // The user opens a buffer in the remote worktree. The buffer's // contents are loaded from the remote filesystem. let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) }) .await .unwrap(); let diff = project .update(cx, |project, cx| { project.open_unstaged_diff(buffer.clone(), cx) }) .await .unwrap(); diff.update(cx, |diff, cx| { assert_eq!( diff.base_text_string(cx).unwrap(), "fn one() -> usize { 0 }" ); }); buffer.update(cx, |buffer, cx| { assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); let ix = buffer.text().find('1').unwrap(); buffer.edit([(ix..ix + 1, "100")], None, cx); }); // The user saves the buffer. The new contents are written to the // remote filesystem. project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await .unwrap(); assert_eq!( fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(), "fn one() -> usize { 100 }" ); // A new file is created in the remote filesystem. The user // sees the new file. fs.save( path!("/code/project1/src/main.rs").as_ref(), &"fn main() {}".into(), Default::default(), ) .await .unwrap(); cx.executor().run_until_parked(); worktree.update(cx, |worktree, _cx| { assert_eq!( worktree.paths().collect::<Vec<_>>(), vec![ rel_path("README.md"), rel_path("src"), rel_path("src/lib.rs"), rel_path("src/main.rs"), ] ); }); // A file that is currently open in a buffer is renamed. fs.rename( path!("/code/project1/src/lib.rs").as_ref(), path!("/code/project1/src/lib2.rs").as_ref(), Default::default(), ) .await .unwrap(); cx.executor().run_until_parked(); buffer.update(cx, |buffer, _| { assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs")); }); fs.set_index_for_repo( Path::new(path!("/code/project1/.git")), &[("src/lib2.rs", "fn one() -> usize { 100 }".into())], ); cx.executor().run_until_parked(); diff.update(cx, |diff, cx| { assert_eq!( diff.base_text_string(cx).unwrap(), "fn one() -> usize { 100 }" ); }); } #[gpui::test] async fn test_remote_telemetry_event_forwarding( cx: &mut TestAppContext, server_cx: &mut TestAppContext, ) { // This mirrors `init_test`, but retains the server-side session so the test // can drive a forwarded telemetry event over the proto channel (as // `init_telemetry_forwarding` does on a real remote server). let server_fs = FakeFs::new(server_cx.executor()); server_fs .insert_tree( path!("/code"), json!({ "project1": { "README.md": "# project 1" } }), ) .await; cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx)); server_cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx)); init_logger(); let (opts, server_session, _) = RemoteClient::fake_server(cx, server_cx); let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); let proxy = Arc::new(ExtensionHostProxy::new()); server_cx.update(HeadlessProject::init); let headless = server_cx.new(|cx| { HeadlessProject::new( crate::HeadlessAppState { session: server_session.clone(), fs: server_fs.clone(), http_client, node_runtime, languages, extension_host_proxy: proxy, startup_time: std::time::Instant::now(), }, false, cx, ) }); let ssh = RemoteClient::connect_mock(opts, cx).await; let project = build_project(ssh, cx); project .update(cx, { let headless = headless.clone(); |_, cx| cx.on_release(|_, _| drop(headless)) }) .detach(); // The remote server forwards a bare `FlexibleEvent` as JSON; mirror that // here by sending the proto message the forwarding task would send. let event_json = json!({ "event_type": "fs_watcher_poll", "event_properties": { "path": "/code/project1" }, }) .to_string(); server_session .send(proto::TelemetryEvent { project_id: proto::REMOTE_SERVER_PROJECT_ID, event_json, }) .unwrap(); cx.executor().run_until_parked(); let events = project.read_with(cx, |project, _| { project.client().telemetry().queued_events() }); assert_eq!( events.len(), 1, "the forwarded event should be reported once" ); let event = &events[0]; assert_eq!(event.event_type, "fs_watcher_poll"); // The event's original properties survive the round-trip. assert_eq!( event.event_properties.get("path"), Some(&serde_json::Value::String("/code/project1".to_string())) ); // The client stamps the remote host metadata it learned at connection time. // The mock connection reports a Linux/x86_64 host over a "mock" connection. assert_eq!( event.event_properties.get("remote"), Some(&serde_json::Value::Bool(true)) ); assert_eq!( event.event_properties.get("remote_connection_type"), Some(&serde_json::Value::String("mock".to_string())) ); assert_eq!( event.event_properties.get("remote_os_name"), Some(&serde_json::Value::String("Linux".to_string())) ); assert_eq!( event.event_properties.get("remote_architecture"), Some(&serde_json::Value::String("x86_64".to_string())) ); } async fn do_search_and_assert( project: &Entity<Project>, query: &str, files_to_include: PathMatcher, match_full_paths: bool, expected_paths: &[&str], mut cx: TestAppContext, ) -> Vec<Entity<Buffer>> { let query = query.to_string(); let receiver = project.update(&mut cx, |project, cx| { project.search( SearchQuery::text( query, false, true, false, files_to_include, Default::default(), match_full_paths, None, ) .unwrap(), cx, ) }); let mut buffers = Vec::new(); for expected_path in expected_paths { let buffer = loop { let response = receiver.rx.recv().await.unwrap(); match response { SearchResult::Buffer { buffer, .. } => break buffer, SearchResult::LimitReached => panic!("incorrect result"), SearchResult::WaitingForScan | SearchResult::Searching => continue, } }; buffer.update(&mut cx, |buffer, cx| { assert_eq!( buffer.file().unwrap().full_path(cx).to_string_lossy(), *expected_path ) }); buffers.push(buffer); } assert!(receiver.rx.recv().await.is_err()); buffers } #[gpui::test] async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { ".git": {}, "README.md": "# project 1", "src": { "lib.rs": "fn one() -> usize { 1 }" } }, }), ) .await; let (project, headless) = init_test(&fs, cx, server_cx).await; project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); cx.run_until_parked(); let buffers = do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await; let buffer = buffers.into_iter().next().unwrap(); // test that the headless server is tracking which buffers we have open correctly. cx.run_until_parked(); headless.update(server_cx, |headless, cx| { assert!(headless.buffer_store.read(cx).has_shared_buffers()) }); do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await; server_cx.run_until_parked(); cx.update(|_| { drop(buffer); }); cx.run_until_parked(); server_cx.run_until_parked(); headless.update(server_cx, |headless, cx| { assert!(!headless.buffer_store.read(cx).has_shared_buffers()) }); do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await; } #[gpui::test] async fn test_remote_project_search_single_cpu( cx: &mut TestAppContext, server_cx: &mut TestAppContext, ) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { ".git": {}, "README.md": "# project 1", "src": { "lib.rs": "fn one() -> usize { 1 }" } }, }), ) .await; // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU). // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers, // which silently drops all search channels and produces zero results. server_cx.executor().set_num_cpus(1); let (project, _) = init_test(&fs, cx, server_cx).await; project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); cx.run_until_parked(); do_search_and_assert( &project, "project", Default::default(), false, &[path!("project1/README.md")], cx.clone(), ) .await; } #[gpui::test] async fn test_remote_project_search_inclusion( cx: &mut TestAppContext, server_cx: &mut TestAppContext, ) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( path!("/code"), json!({ "project1": { "README.md": "# project 1", }, "project2": { "README.md": "# project 2", }, }), ) .await; let (project, _) = init_test(&fs, cx, server_cx).await; project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/code/project2"), true, cx) }) .await .unwrap(); cx.run_until_parked();