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