remote_editing_collaboration_tests.rs

  1use crate::tests::TestServer;
  2use call::ActiveCall;
  3use collections::{HashMap, HashSet};
  4
  5use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
  6use debugger_ui::debugger_panel::DebugPanel;
  7use extension::ExtensionHostProxy;
  8use fs::{FakeFs, Fs as _, RemoveOptions};
  9use futures::StreamExt as _;
 10use gpui::{
 11    AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
 12    VisualContext,
 13};
 14use http_client::BlockedHttpClient;
 15use language::{
 16    FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
 17    language_settings::{
 18        Formatter, FormatterList, PrettierSettings, SelectedFormatter, language_settings,
 19    },
 20    tree_sitter_typescript,
 21};
 22use node_runtime::NodeRuntime;
 23use project::{
 24    ProjectPath,
 25    debugger::session::ThreadId,
 26    lsp_store::{FormatTrigger, LspFormatTarget},
 27};
 28use remote::RemoteClient;
 29use remote_server::{HeadlessAppState, HeadlessProject};
 30use rpc::proto;
 31use serde_json::json;
 32use settings::{PrettierSettingsContent, SettingsStore};
 33use std::{
 34    path::Path,
 35    sync::{Arc, atomic::AtomicUsize},
 36};
 37use task::TcpArgumentsTemplate;
 38use util::path;
 39
 40#[gpui::test(iterations = 10)]
 41async fn test_sharing_an_ssh_remote_project(
 42    cx_a: &mut TestAppContext,
 43    cx_b: &mut TestAppContext,
 44    server_cx: &mut TestAppContext,
 45) {
 46    let executor = cx_a.executor();
 47    cx_a.update(|cx| {
 48        release_channel::init(SemanticVersion::default(), cx);
 49    });
 50    server_cx.update(|cx| {
 51        release_channel::init(SemanticVersion::default(), cx);
 52    });
 53    let mut server = TestServer::start(executor.clone()).await;
 54    let client_a = server.create_client(cx_a, "user_a").await;
 55    let client_b = server.create_client(cx_b, "user_b").await;
 56    server
 57        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 58        .await;
 59
 60    // Set up project on remote FS
 61    let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
 62    let remote_fs = FakeFs::new(server_cx.executor());
 63    remote_fs
 64        .insert_tree(
 65            path!("/code"),
 66            json!({
 67                "project1": {
 68                    ".zed": {
 69                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
 70                    },
 71                    "README.md": "# project 1",
 72                    "src": {
 73                        "lib.rs": "fn one() -> usize { 1 }"
 74                    }
 75                },
 76                "project2": {
 77                    "README.md": "# project 2",
 78                },
 79            }),
 80        )
 81        .await;
 82
 83    // User A connects to the remote project via SSH.
 84    server_cx.update(HeadlessProject::init);
 85    let remote_http_client = Arc::new(BlockedHttpClient);
 86    let node = NodeRuntime::unavailable();
 87    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 88    let _headless_project = server_cx.new(|cx| {
 89        client::init_settings(cx);
 90        HeadlessProject::new(
 91            HeadlessAppState {
 92                session: server_ssh,
 93                fs: remote_fs.clone(),
 94                http_client: remote_http_client,
 95                node_runtime: node,
 96                languages,
 97                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 98            },
 99            cx,
100        )
101    });
102
103    let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
104    let (project_a, worktree_id) = client_a
105        .build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
106        .await;
107
108    // While the SSH worktree is being scanned, user A shares the remote project.
109    let active_call_a = cx_a.read(ActiveCall::global);
110    let project_id = active_call_a
111        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
112        .await
113        .unwrap();
114
115    // User B joins the project.
116    let project_b = client_b.join_remote_project(project_id, cx_b).await;
117    let worktree_b = project_b
118        .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
119        .unwrap();
120
121    let worktree_a = project_a
122        .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
123        .unwrap();
124
125    executor.run_until_parked();
126
127    worktree_a.update(cx_a, |worktree, _cx| {
128        assert_eq!(
129            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
130            vec![
131                Path::new(".zed"),
132                Path::new(".zed/settings.json"),
133                Path::new("README.md"),
134                Path::new("src"),
135                Path::new("src/lib.rs"),
136            ]
137        );
138    });
139
140    worktree_b.update(cx_b, |worktree, _cx| {
141        assert_eq!(
142            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
143            vec![
144                Path::new(".zed"),
145                Path::new(".zed/settings.json"),
146                Path::new("README.md"),
147                Path::new("src"),
148                Path::new("src/lib.rs"),
149            ]
150        );
151    });
152
153    // User B can open buffers in the remote project.
154    let buffer_b = project_b
155        .update(cx_b, |project, cx| {
156            project.open_buffer((worktree_id, "src/lib.rs"), cx)
157        })
158        .await
159        .unwrap();
160    buffer_b.update(cx_b, |buffer, cx| {
161        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
162        let ix = buffer.text().find('1').unwrap();
163        buffer.edit([(ix..ix + 1, "100")], None, cx);
164    });
165
166    executor.run_until_parked();
167
168    cx_b.read(|cx| {
169        let file = buffer_b.read(cx).file();
170        assert_eq!(
171            language_settings(Some("Rust".into()), file, cx).language_servers,
172            ["override-rust-analyzer".to_string()]
173        )
174    });
175
176    project_b
177        .update(cx_b, |project, cx| {
178            project.save_buffer_as(
179                buffer_b.clone(),
180                ProjectPath {
181                    worktree_id: worktree_id.to_owned(),
182                    path: Arc::from(Path::new("src/renamed.rs")),
183                },
184                cx,
185            )
186        })
187        .await
188        .unwrap();
189    assert_eq!(
190        remote_fs
191            .load(path!("/code/project1/src/renamed.rs").as_ref())
192            .await
193            .unwrap(),
194        "fn one() -> usize { 100 }"
195    );
196    cx_b.run_until_parked();
197    cx_b.update(|cx| {
198        assert_eq!(
199            buffer_b
200                .read(cx)
201                .file()
202                .unwrap()
203                .path()
204                .to_string_lossy()
205                .to_string(),
206            path!("src/renamed.rs").to_string()
207        );
208    });
209}
210
211#[gpui::test]
212async fn test_ssh_collaboration_git_branches(
213    executor: BackgroundExecutor,
214    cx_a: &mut TestAppContext,
215    cx_b: &mut TestAppContext,
216    server_cx: &mut TestAppContext,
217) {
218    cx_a.set_name("a");
219    cx_b.set_name("b");
220    server_cx.set_name("server");
221
222    cx_a.update(|cx| {
223        release_channel::init(SemanticVersion::default(), cx);
224    });
225    server_cx.update(|cx| {
226        release_channel::init(SemanticVersion::default(), cx);
227    });
228
229    let mut server = TestServer::start(executor.clone()).await;
230    let client_a = server.create_client(cx_a, "user_a").await;
231    let client_b = server.create_client(cx_b, "user_b").await;
232    server
233        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
234        .await;
235
236    // Set up project on remote FS
237    let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
238    let remote_fs = FakeFs::new(server_cx.executor());
239    remote_fs
240        .insert_tree("/project", serde_json::json!({ ".git":{} }))
241        .await;
242
243    let branches = ["main", "dev", "feature-1"];
244    let branches_set = branches
245        .iter()
246        .map(ToString::to_string)
247        .collect::<HashSet<_>>();
248    remote_fs.insert_branches(Path::new("/project/.git"), &branches);
249
250    // User A connects to the remote project via SSH.
251    server_cx.update(HeadlessProject::init);
252    let remote_http_client = Arc::new(BlockedHttpClient);
253    let node = NodeRuntime::unavailable();
254    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
255    let headless_project = server_cx.new(|cx| {
256        client::init_settings(cx);
257        HeadlessProject::new(
258            HeadlessAppState {
259                session: server_ssh,
260                fs: remote_fs.clone(),
261                http_client: remote_http_client,
262                node_runtime: node,
263                languages,
264                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
265            },
266            cx,
267        )
268    });
269
270    let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
271    let (project_a, _) = client_a
272        .build_ssh_project("/project", client_ssh, cx_a)
273        .await;
274
275    // While the SSH worktree is being scanned, user A shares the remote project.
276    let active_call_a = cx_a.read(ActiveCall::global);
277    let project_id = active_call_a
278        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
279        .await
280        .unwrap();
281
282    // User B joins the project.
283    let project_b = client_b.join_remote_project(project_id, cx_b).await;
284
285    // Give client A sometime to see that B has joined, and that the headless server
286    // has some git repositories
287    executor.run_until_parked();
288
289    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
290
291    let branches_b = cx_b
292        .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
293        .await
294        .unwrap()
295        .unwrap();
296
297    let new_branch = branches[2];
298
299    let branches_b = branches_b
300        .into_iter()
301        .map(|branch| branch.name().to_string())
302        .collect::<HashSet<_>>();
303
304    assert_eq!(&branches_b, &branches_set);
305
306    cx_b.update(|cx| {
307        repo_b.update(cx, |repo_b, _cx| {
308            repo_b.change_branch(new_branch.to_string())
309        })
310    })
311    .await
312    .unwrap()
313    .unwrap();
314
315    executor.run_until_parked();
316
317    let server_branch = server_cx.update(|cx| {
318        headless_project.update(cx, |headless_project, cx| {
319            headless_project.git_store.update(cx, |git_store, cx| {
320                git_store
321                    .repositories()
322                    .values()
323                    .next()
324                    .unwrap()
325                    .read(cx)
326                    .branch
327                    .as_ref()
328                    .unwrap()
329                    .clone()
330            })
331        })
332    });
333
334    assert_eq!(server_branch.name(), branches[2]);
335
336    // Also try creating a new branch
337    cx_b.update(|cx| {
338        repo_b.update(cx, |repo_b, _cx| {
339            repo_b.create_branch("totally-new-branch".to_string())
340        })
341    })
342    .await
343    .unwrap()
344    .unwrap();
345
346    cx_b.update(|cx| {
347        repo_b.update(cx, |repo_b, _cx| {
348            repo_b.change_branch("totally-new-branch".to_string())
349        })
350    })
351    .await
352    .unwrap()
353    .unwrap();
354
355    executor.run_until_parked();
356
357    let server_branch = server_cx.update(|cx| {
358        headless_project.update(cx, |headless_project, cx| {
359            headless_project.git_store.update(cx, |git_store, cx| {
360                git_store
361                    .repositories()
362                    .values()
363                    .next()
364                    .unwrap()
365                    .read(cx)
366                    .branch
367                    .as_ref()
368                    .unwrap()
369                    .clone()
370            })
371        })
372    });
373
374    assert_eq!(server_branch.name(), "totally-new-branch");
375
376    // Remove the git repository and check that all participants get the update.
377    remote_fs
378        .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
379        .await
380        .unwrap();
381    executor.run_until_parked();
382
383    project_a.update(cx_a, |project, cx| {
384        pretty_assertions::assert_eq!(
385            project.git_store().read(cx).repo_snapshots(cx),
386            HashMap::default()
387        );
388    });
389    project_b.update(cx_b, |project, cx| {
390        pretty_assertions::assert_eq!(
391            project.git_store().read(cx).repo_snapshots(cx),
392            HashMap::default()
393        );
394    });
395}
396
397#[gpui::test]
398async fn test_ssh_collaboration_formatting_with_prettier(
399    executor: BackgroundExecutor,
400    cx_a: &mut TestAppContext,
401    cx_b: &mut TestAppContext,
402    server_cx: &mut TestAppContext,
403) {
404    cx_a.set_name("a");
405    cx_b.set_name("b");
406    server_cx.set_name("server");
407
408    cx_a.update(|cx| {
409        release_channel::init(SemanticVersion::default(), cx);
410    });
411    server_cx.update(|cx| {
412        release_channel::init(SemanticVersion::default(), cx);
413    });
414
415    let mut server = TestServer::start(executor.clone()).await;
416    let client_a = server.create_client(cx_a, "user_a").await;
417    let client_b = server.create_client(cx_b, "user_b").await;
418    server
419        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
420        .await;
421
422    let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
423    let remote_fs = FakeFs::new(server_cx.executor());
424    let buffer_text = "let one = \"two\"";
425    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
426    remote_fs
427        .insert_tree(
428            path!("/project"),
429            serde_json::json!({ "a.ts": buffer_text }),
430        )
431        .await;
432
433    let test_plugin = "test_plugin";
434    let ts_lang = Arc::new(Language::new(
435        LanguageConfig {
436            name: "TypeScript".into(),
437            matcher: LanguageMatcher {
438                path_suffixes: vec!["ts".to_string()],
439                ..LanguageMatcher::default()
440            },
441            ..LanguageConfig::default()
442        },
443        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
444    ));
445    client_a.language_registry().add(ts_lang.clone());
446    client_b.language_registry().add(ts_lang.clone());
447
448    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
449    let mut fake_language_servers = languages.register_fake_lsp(
450        "TypeScript",
451        FakeLspAdapter {
452            prettier_plugins: vec![test_plugin],
453            ..Default::default()
454        },
455    );
456
457    // User A connects to the remote project via SSH.
458    server_cx.update(HeadlessProject::init);
459    let remote_http_client = Arc::new(BlockedHttpClient);
460    let _headless_project = server_cx.new(|cx| {
461        client::init_settings(cx);
462        HeadlessProject::new(
463            HeadlessAppState {
464                session: server_ssh,
465                fs: remote_fs.clone(),
466                http_client: remote_http_client,
467                node_runtime: NodeRuntime::unavailable(),
468                languages,
469                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
470            },
471            cx,
472        )
473    });
474
475    let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
476    let (project_a, worktree_id) = client_a
477        .build_ssh_project(path!("/project"), client_ssh, cx_a)
478        .await;
479
480    // While the SSH worktree is being scanned, user A shares the remote project.
481    let active_call_a = cx_a.read(ActiveCall::global);
482    let project_id = active_call_a
483        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
484        .await
485        .unwrap();
486
487    // User B joins the project.
488    let project_b = client_b.join_remote_project(project_id, cx_b).await;
489    executor.run_until_parked();
490
491    // Opens the buffer and formats it
492    let (buffer_b, _handle) = project_b
493        .update(cx_b, |p, cx| {
494            p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
495        })
496        .await
497        .expect("user B opens buffer for formatting");
498
499    cx_a.update(|cx| {
500        SettingsStore::update_global(cx, |store, cx| {
501            store.update_user_settings(cx, |file| {
502                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
503                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
504                    allowed: true,
505                    ..Default::default()
506                });
507            });
508        });
509    });
510    cx_b.update(|cx| {
511        SettingsStore::update_global(cx, |store, cx| {
512            store.update_user_settings(cx, |file| {
513                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
514                    FormatterList::Single(Formatter::LanguageServer { name: None }),
515                ));
516                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
517                    allowed: true,
518                    ..Default::default()
519                });
520            });
521        });
522    });
523    let fake_language_server = fake_language_servers.next().await.unwrap();
524    fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
525        panic!(
526            "Unexpected: prettier should be preferred since it's enabled and language supports it"
527        )
528    });
529
530    project_b
531        .update(cx_b, |project, cx| {
532            project.format(
533                HashSet::from_iter([buffer_b.clone()]),
534                LspFormatTarget::Buffers,
535                true,
536                FormatTrigger::Save,
537                cx,
538            )
539        })
540        .await
541        .unwrap();
542
543    executor.run_until_parked();
544    assert_eq!(
545        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
546        buffer_text.to_string() + "\n" + prettier_format_suffix,
547        "Prettier formatting was not applied to client buffer after client's request"
548    );
549
550    // User A opens and formats the same buffer too
551    let buffer_a = project_a
552        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
553        .await
554        .expect("user A opens buffer for formatting");
555
556    cx_a.update(|cx| {
557        SettingsStore::update_global(cx, |store, cx| {
558            store.update_user_settings(cx, |file| {
559                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
560                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
561                    allowed: true,
562                    ..Default::default()
563                });
564            });
565        });
566    });
567    project_a
568        .update(cx_a, |project, cx| {
569            project.format(
570                HashSet::from_iter([buffer_a.clone()]),
571                LspFormatTarget::Buffers,
572                true,
573                FormatTrigger::Manual,
574                cx,
575            )
576        })
577        .await
578        .unwrap();
579
580    executor.run_until_parked();
581    assert_eq!(
582        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
583        buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
584        "Prettier formatting was not applied to client buffer after host's request"
585    );
586}
587
588#[gpui::test]
589async fn test_remote_server_debugger(
590    cx_a: &mut TestAppContext,
591    server_cx: &mut TestAppContext,
592    executor: BackgroundExecutor,
593) {
594    cx_a.update(|cx| {
595        release_channel::init(SemanticVersion::default(), cx);
596        command_palette_hooks::init(cx);
597        zlog::init_test();
598        dap_adapters::init(cx);
599    });
600    server_cx.update(|cx| {
601        release_channel::init(SemanticVersion::default(), cx);
602        dap_adapters::init(cx);
603    });
604    let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
605    let remote_fs = FakeFs::new(server_cx.executor());
606    remote_fs
607        .insert_tree(
608            path!("/code"),
609            json!({
610                "lib.rs": "fn one() -> usize { 1 }"
611            }),
612        )
613        .await;
614
615    // User A connects to the remote project via SSH.
616    server_cx.update(HeadlessProject::init);
617    let remote_http_client = Arc::new(BlockedHttpClient);
618    let node = NodeRuntime::unavailable();
619    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
620    let _headless_project = server_cx.new(|cx| {
621        client::init_settings(cx);
622        HeadlessProject::new(
623            HeadlessAppState {
624                session: server_ssh,
625                fs: remote_fs.clone(),
626                http_client: remote_http_client,
627                node_runtime: node,
628                languages,
629                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
630            },
631            cx,
632        )
633    });
634
635    let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
636    let mut server = TestServer::start(server_cx.executor()).await;
637    let client_a = server.create_client(cx_a, "user_a").await;
638    cx_a.update(|cx| {
639        debugger_ui::init(cx);
640        command_palette_hooks::init(cx);
641    });
642    let (project_a, _) = client_a
643        .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
644        .await;
645
646    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
647
648    let debugger_panel = workspace
649        .update_in(cx_a, |_workspace, window, cx| {
650            cx.spawn_in(window, DebugPanel::load)
651        })
652        .await
653        .unwrap();
654
655    workspace.update_in(cx_a, |workspace, window, cx| {
656        workspace.add_panel(debugger_panel, window, cx);
657    });
658
659    cx_a.run_until_parked();
660    let debug_panel = workspace
661        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
662        .unwrap();
663
664    let workspace_window = cx_a
665        .window_handle()
666        .downcast::<workspace::Workspace>()
667        .unwrap();
668
669    let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
670    cx_a.run_until_parked();
671    debug_panel.update(cx_a, |debug_panel, cx| {
672        assert_eq!(
673            debug_panel.active_session().unwrap().read(cx).session(cx),
674            session
675        )
676    });
677
678    session.update(cx_a, |session, _| {
679        assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
680    });
681
682    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
683        workspace.project().update(cx, |project, cx| {
684            project.dap_store().update(cx, |dap_store, cx| {
685                dap_store.shutdown_session(session.read(cx).session_id(), cx)
686            })
687        })
688    });
689
690    client_ssh.update(cx_a, |a, _| {
691        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
692    });
693
694    shutdown_session.await.unwrap();
695}
696
697#[gpui::test]
698async fn test_slow_adapter_startup_retries(
699    cx_a: &mut TestAppContext,
700    server_cx: &mut TestAppContext,
701    executor: BackgroundExecutor,
702) {
703    cx_a.update(|cx| {
704        release_channel::init(SemanticVersion::default(), cx);
705        command_palette_hooks::init(cx);
706        zlog::init_test();
707        dap_adapters::init(cx);
708    });
709    server_cx.update(|cx| {
710        release_channel::init(SemanticVersion::default(), cx);
711        dap_adapters::init(cx);
712    });
713    let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
714    let remote_fs = FakeFs::new(server_cx.executor());
715    remote_fs
716        .insert_tree(
717            path!("/code"),
718            json!({
719                "lib.rs": "fn one() -> usize { 1 }"
720            }),
721        )
722        .await;
723
724    // User A connects to the remote project via SSH.
725    server_cx.update(HeadlessProject::init);
726    let remote_http_client = Arc::new(BlockedHttpClient);
727    let node = NodeRuntime::unavailable();
728    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
729    let _headless_project = server_cx.new(|cx| {
730        client::init_settings(cx);
731        HeadlessProject::new(
732            HeadlessAppState {
733                session: server_ssh,
734                fs: remote_fs.clone(),
735                http_client: remote_http_client,
736                node_runtime: node,
737                languages,
738                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
739            },
740            cx,
741        )
742    });
743
744    let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
745    let mut server = TestServer::start(server_cx.executor()).await;
746    let client_a = server.create_client(cx_a, "user_a").await;
747    cx_a.update(|cx| {
748        debugger_ui::init(cx);
749        command_palette_hooks::init(cx);
750    });
751    let (project_a, _) = client_a
752        .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
753        .await;
754
755    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
756
757    let debugger_panel = workspace
758        .update_in(cx_a, |_workspace, window, cx| {
759            cx.spawn_in(window, DebugPanel::load)
760        })
761        .await
762        .unwrap();
763
764    workspace.update_in(cx_a, |workspace, window, cx| {
765        workspace.add_panel(debugger_panel, window, cx);
766    });
767
768    cx_a.run_until_parked();
769    let debug_panel = workspace
770        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
771        .unwrap();
772
773    let workspace_window = cx_a
774        .window_handle()
775        .downcast::<workspace::Workspace>()
776        .unwrap();
777
778    let count = Arc::new(AtomicUsize::new(0));
779    let session = debugger_ui::tests::start_debug_session_with(
780        &workspace_window,
781        cx_a,
782        DebugTaskDefinition {
783            adapter: "fake-adapter".into(),
784            label: "test".into(),
785            config: json!({
786                "request": "launch"
787            }),
788            tcp_connection: Some(TcpArgumentsTemplate {
789                port: None,
790                host: None,
791                timeout: None,
792            }),
793        },
794        move |client| {
795            let count = count.clone();
796            client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
797                if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
798                    return RequestHandling::Exit;
799                }
800                RequestHandling::Respond(Ok(Capabilities::default()))
801            });
802        },
803    )
804    .unwrap();
805    cx_a.run_until_parked();
806
807    let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
808    client
809        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
810            reason: dap::StoppedEventReason::Pause,
811            description: None,
812            thread_id: Some(1),
813            preserve_focus_hint: None,
814            text: None,
815            all_threads_stopped: None,
816            hit_breakpoint_ids: None,
817        }))
818        .await;
819
820    cx_a.run_until_parked();
821
822    let active_session = debug_panel
823        .update(cx_a, |this, _| this.active_session())
824        .unwrap();
825
826    let running_state = active_session.update(cx_a, |active_session, _| {
827        active_session.running_state().clone()
828    });
829
830    assert_eq!(
831        client.id(),
832        running_state.read_with(cx_a, |running_state, _| running_state.session_id())
833    );
834    assert_eq!(
835        ThreadId(1),
836        running_state.read_with(cx_a, |running_state, _| running_state
837            .selected_thread_id()
838            .unwrap())
839    );
840
841    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
842        workspace.project().update(cx, |project, cx| {
843            project.dap_store().update(cx, |dap_store, cx| {
844                dap_store.shutdown_session(session.read(cx).session_id(), cx)
845            })
846        })
847    });
848
849    client_ssh.update(cx_a, |a, _| {
850        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
851    });
852
853    shutdown_session.await.unwrap();
854}