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