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