diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 67d19754c13c12599edc548335fc559cce2bba18..a79313000a1b8341001d47dcc07a0920e103798a 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -5802,19 +5802,16 @@ async fn test_remote_project_integration_does_not_briefly_render_as_separate_pro .await; server_fs.set_branch_name(Path::new("/project/.git"), Some("main")); - // Create a linked worktree on the remote server so that opening - // /project-wt-1 succeeds and the worktree has a .git file pointing - // back to the main repo. + // Create the linked worktree checkout path on the remote server, + // but do not yet register it as a git-linked worktree. The real + // regrouping update in this test should happen only after the + // sidebar opens the closed remote thread. server_fs - .add_linked_worktree_for_repo( - Path::new("/project/.git"), - false, - git::repository::Worktree { - path: PathBuf::from("/project-wt-1"), - ref_name: Some("refs/heads/feature-wt".into()), - sha: "abc123".into(), - is_main: false, - }, + .insert_tree( + "/project-wt-1", + serde_json::json!({ + "src": { "main.rs": "fn main() {}" } + }), ) .await; @@ -5926,44 +5923,129 @@ async fn test_remote_project_integration_does_not_briefly_render_as_separate_pro }); cx.run_until_parked(); - let main_thread_id_for_observer = main_thread_id.clone(); - let remote_thread_id_for_observer = remote_thread_id.clone(); + focus_sidebar(&sidebar, cx); + sidebar.update_in(cx, |sidebar, _window, _cx| { + sidebar.selection = sidebar.contents.entries.iter().position(|entry| { + matches!( + entry, + ListEntry::Thread(thread) if thread.metadata.session_id == remote_thread_id + ) + }); + }); + + let saw_separate_project_header = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let saw_separate_project_header_for_observer = saw_separate_project_header.clone(); sidebar .update(cx, |_, cx| { cx.observe_self(move |sidebar, _cx| { - assert_remote_project_integration_sidebar_state( - sidebar, - &main_thread_id_for_observer, - &remote_thread_id_for_observer, - ); + 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 { + saw_separate_project_header_for_observer + .store(true, std::sync::atomic::Ordering::SeqCst); + return; + }; + + if project_header != "project" || project_headers.next().is_some() { + saw_separate_project_header_for_observer + .store(true, std::sync::atomic::Ordering::SeqCst); + } }) }) .detach(); - // Simulate what happens in production when a new remote workspace - // is opened for a linked worktree: insert_workspace() computes - // project_group_key() before root_repo_common_dir is populated. - // The fallback uses abs_path(), producing key ("/project-wt-1") - // instead of the correct ("/project"). - let remote_host = project.read_with(cx, |p, cx| p.remote_connection_options(cx)); - let stale_key = ProjectGroupKey::new( - remote_host, - PathList::new(&[PathBuf::from("/project-wt-1")]), - ); - multi_workspace.update(cx, |mw, _cx| { - mw.add_project_group_key(stale_key); + multi_workspace.update(cx, |multi_workspace, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace: &mut Workspace, cx| { + let remote_client = workspace + .project() + .read(cx) + .remote_client() + .expect("main remote project should have a remote client"); + remote_client.update(cx, |remote_client: &mut remote::RemoteClient, cx| { + remote_client.force_server_not_running(cx); + }); + }); + }); + cx.run_until_parked(); + + let (server_session_2, connect_guard_2) = + remote::RemoteClient::fake_server_with_opts(&original_opts, cx, server_cx); + let _headless_2 = server_cx.new(|cx| { + remote_server::HeadlessProject::new( + remote_server::HeadlessAppState { + session: server_session_2, + fs: server_fs.clone(), + 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, + ) }); + drop(connect_guard_2); + + let window = cx.windows()[0]; + cx.update_window(window, |_, window, cx| { + window.dispatch_action(Confirm.boxed_clone(), cx); + }) + .unwrap(); - // Force the sidebar to rebuild immediately from the current - // MultiWorkspace state so the observer can detect transient - // duplicate headers instead of only checking the final settled view. - sidebar.update(cx, |sidebar, cx| { - sidebar.update_entries(cx); + cx.run_until_parked(); + + let new_workspace = multi_workspace.read_with(cx, |mw, _| { + assert_eq!( + mw.workspaces().count(), + 2, + "confirming a closed remote thread should open a second workspace" + ); + mw.workspaces() + .find(|workspace| workspace.entity_id() != mw.workspace().entity_id()) + .unwrap() + .clone() }); + server_fs + .add_linked_worktree_for_repo( + Path::new("/project/.git"), + true, + git::repository::Worktree { + path: PathBuf::from("/project-wt-1"), + ref_name: Some("refs/heads/feature-wt".into()), + sha: "abc123".into(), + is_main: false, + }, + ) + .await; + + server_cx.run_until_parked(); + cx.run_until_parked(); + server_cx.run_until_parked(); cx.run_until_parked(); + let entries_after_update = visible_entries_as_strings(&sidebar, cx); + let group_after_update = new_workspace.read_with(cx, |workspace, cx| { + workspace.project().read(cx).project_group_key(cx) + }); + + assert_eq!( + group_after_update, + project.read_with(cx, |project, cx| project.project_group_key(cx)), + "expected the remote worktree workspace to be grouped under the main remote project after the real update; \ + final sidebar entries: {:?}", + entries_after_update, + ); + sidebar.update(cx, |sidebar, _cx| { assert_remote_project_integration_sidebar_state( sidebar, @@ -5971,4 +6053,12 @@ async fn test_remote_project_integration_does_not_briefly_render_as_separate_pro &remote_thread_id, ); }); + + assert!( + !saw_separate_project_header.load(std::sync::atomic::Ordering::SeqCst), + "sidebar briefly rendered the remote worktree as a separate project during the real remote open/update sequence; \ + final group: {:?}; final sidebar entries: {:?}", + group_after_update, + entries_after_update, + ); }