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