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