remote: Introduce a proper mock remote connection (#46337)

Lukas Wirth created

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

Cargo.lock                                                    |   1 
crates/collab/Cargo.toml                                      |   1 
crates/collab/src/tests/remote_editing_collaboration_tests.rs |   2 
crates/collab_ui/Cargo.toml                                   |   1 
crates/dap/src/client.rs                                      |   4 
crates/dap/src/transport.rs                                   |   5 
crates/git_ui/Cargo.toml                                      |   2 
crates/project/Cargo.toml                                     |   2 
crates/project/src/trusted_worktrees.rs                       |   4 
crates/recent_projects/Cargo.toml                             |   5 
crates/recent_projects/src/recent_projects.rs                 |   2 
crates/recent_projects/src/remote_connections.rs              |  12 
crates/recent_projects/src/remote_servers.rs                  |   5 
crates/remote/src/remote.rs                                   |   5 
crates/remote/src/remote_client.rs                            | 303 ---
crates/remote/src/transport.rs                                |   2 
crates/remote/src/transport/mock.rs                           | 312 +++++
crates/remote_server/src/remote_editing_tests.rs              |   2 
crates/title_bar/Cargo.toml                                   |   2 
crates/title_bar/src/title_bar.rs                             |   2 
crates/workspace/src/persistence.rs                           |   5 
crates/zed/Cargo.toml                                         |   1 
22 files changed, 426 insertions(+), 254 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3372,6 +3372,7 @@ dependencies = [
  "text",
  "theme",
  "time",
+ "title_bar",
  "tokio",
  "toml 0.8.23",
  "tower 0.4.13",

crates/collab/Cargo.toml 🔗

@@ -125,6 +125,7 @@ smol.workspace = true
 sqlx = { version = "0.8", features = ["sqlite"] }
 task.workspace = true
 theme.workspace = true
+title_bar = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
 util.workspace = true
 workspace = { workspace = true, features = ["test-support"] }

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -676,7 +676,7 @@ async fn test_remote_server_debugger(
     });
 
     session.update(cx_a, |session, _| {
-        assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
+        assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
     });
 
     let shutdown_session = workspace.update(cx_a, |workspace, cx| {

crates/collab_ui/Cargo.toml 🔗

@@ -26,6 +26,7 @@ test-support = [
     "util/test-support",
     "workspace/test-support",
     "http_client/test-support",
+    "title_bar/test-support",
 ]
 
 [dependencies]

crates/dap/src/client.rs 🔗

@@ -59,7 +59,9 @@ impl DebugAdapterClient {
 
     pub fn should_reconnect_for_ssh(&self) -> bool {
         self.transport_delegate.tcp_arguments().is_some()
-            && self.binary.command.as_deref() == Some("ssh")
+            && (self.binary.command.as_deref() == Some("ssh")
+                || (cfg!(feature = "test-support")
+                    && self.binary.command.as_deref() == Some("mock")))
     }
 
     pub async fn connect(

crates/dap/src/transport.rs 🔗

@@ -221,10 +221,7 @@ impl TransportDelegate {
             }));
         }
 
-        {
-            let mut lock = self.server_tx.lock().await;
-            *lock = Some(server_tx.clone());
-        }
+        *self.server_tx.lock().await = Some(server_tx.clone());
 
         Ok(())
     }

crates/git_ui/Cargo.toml 🔗

@@ -13,7 +13,7 @@ name = "git_ui"
 path = "src/git_ui.rs"
 
 [features]
-test-support = ["multi_buffer/test-support"]
+test-support = ["multi_buffer/test-support", "recent_projects/test-support"]
 
 [dependencies]
 agent_settings.workspace = true

crates/project/Cargo.toml 🔗

@@ -17,6 +17,7 @@ test-support = [
     "buffer_diff/test-support",
     "client/test-support",
     "language/test-support",
+    "remote/test-support",
     "settings/test-support",
     "snippet_provider/test-support",
     "text/test-support",
@@ -117,6 +118,7 @@ snippet_provider = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
 util = { workspace = true, features = ["test-support"] }
 worktree = { workspace = true, features = ["test-support"] }
+remote = { workspace = true, features = ["test-support"] }
 
 [package.metadata.cargo-machete]
 ignored = ["tracing"]

crates/project/src/trusted_worktrees.rs 🔗

@@ -162,6 +162,10 @@ impl From<RemoteConnectionOptions> for RemoteHostLocation {
                 Some(SharedString::new(docker_connection_options.name)),
                 SharedString::new(docker_connection_options.container_id),
             ),
+            #[cfg(any(test, feature = "test-support"))]
+            RemoteConnectionOptions::Mock(mock) => {
+                (None, SharedString::new(format!("mock-{}", mock.id)))
+            }
         };
         Self {
             user_name,

crates/recent_projects/Cargo.toml 🔗

@@ -12,6 +12,10 @@ workspace = true
 path = "src/recent_projects.rs"
 doctest = false
 
+[features]
+default = []
+test-support = ["remote/test-support"]
+
 [dependencies]
 anyhow.workspace = true
 askpass.workspace = true
@@ -60,3 +64,4 @@ project = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
 settings = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }
+remote = { workspace = true, features = ["test-support"] }

crates/recent_projects/src/recent_projects.rs 🔗

@@ -719,6 +719,8 @@ impl PickerDelegate for RecentProjectsDelegate {
                                         RemoteConnectionOptions::Ssh { .. } => IconName::Server,
                                         RemoteConnectionOptions::Wsl { .. } => IconName::Linux,
                                         RemoteConnectionOptions::Docker(_) => IconName::Box,
+                                        #[cfg(any(test, feature = "test-support"))]
+                                        RemoteConnectionOptions::Mock(_) => IconName::Server,
                                     })
                                     .color(Color::Muted)
                                     .into_any_element()

crates/recent_projects/src/remote_connections.rs 🔗

@@ -309,6 +309,10 @@ impl RemoteConnectionModal {
                 (options.distro_name.clone(), None, true, false)
             }
             RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true),
+            #[cfg(any(test, feature = "test-support"))]
+            RemoteConnectionOptions::Mock(options) => {
+                (format!("mock-{}", options.id), None, false, false)
+            }
         };
         Self {
             prompt: cx.new(|cx| {
@@ -720,6 +724,10 @@ pub async fn open_remote_project(
                                     RemoteConnectionOptions::Docker(_) => {
                                         "Failed to connect to Dev Container"
                                     }
+                                    #[cfg(any(test, feature = "test-support"))]
+                                    RemoteConnectionOptions::Mock(_) => {
+                                        "Failed to connect to mock server"
+                                    }
                                 },
                                 Some(&format!("{e:#}")),
                                 &["Retry", "Cancel"],
@@ -779,6 +787,10 @@ pub async fn open_remote_project(
                                 RemoteConnectionOptions::Docker(_) => {
                                     "Failed to connect to Dev Container"
                                 }
+                                #[cfg(any(test, feature = "test-support"))]
+                                RemoteConnectionOptions::Mock(_) => {
+                                    "Failed to connect to mock server"
+                                }
                             },
                             Some(&format!("{e:#}")),
                             &["Retry", "Cancel"],

crates/recent_projects/src/remote_servers.rs 🔗

@@ -245,6 +245,11 @@ impl ProjectPicker {
                 connection_string: "".into(),
                 nickname: None,
             },
+            #[cfg(any(test, feature = "test-support"))]
+            RemoteConnectionOptions::Mock(options) => ProjectPickerData::Ssh {
+                connection_string: format!("mock-{}", options.id).into(),
+                nickname: None,
+            },
         };
         let _path_task = cx
             .spawn_in(window, {

crates/remote/src/remote.rs 🔗

@@ -14,3 +14,8 @@ pub use remote_client::{
 pub use transport::docker::DockerConnectionOptions;
 pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
 pub use transport::wsl::WslConnectionOptions;
+
+#[cfg(any(test, feature = "test-support"))]
+pub use transport::mock::{
+    MockConnection, MockConnectionOptions, MockConnectionRegistry, MockDelegate,
+};

crates/remote/src/remote_client.rs 🔗

@@ -958,71 +958,58 @@ impl RemoteClient {
     pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> {
         let opts = self.connection_options();
         client_cx.spawn(async move |cx| {
-            let connection = cx
-                .update_global(|c: &mut ConnectionPool, _| {
-                    if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) {
-                        c.clone()
+            let connection = cx.update_global(|c: &mut ConnectionPool, _| {
+                if let Some(ConnectionPoolEntry::Connected(c)) = c.connections.get(&opts) {
+                    if let Some(connection) = c.upgrade() {
+                        connection
                     } else {
-                        panic!("missing test connection")
+                        panic!("connection was dropped")
                     }
-                })
-                .await
-                .unwrap();
+                } else {
+                    panic!("missing test connection")
+                }
+            });
 
             connection.simulate_disconnect(cx);
         })
     }
 
+    /// Creates a mock connection pair for testing.
+    ///
+    /// This is the recommended way to create mock remote connections for tests.
+    /// It returns both the `MockConnectionOptions` (which can be passed to create
+    /// a `HeadlessProject`) and an `AnyProtoClient` for the server side.
+    ///
+    /// # Example
+    /// ```ignore
+    /// let (opts, server_session) = RemoteClient::fake_server(cx, server_cx);
+    /// // Set up HeadlessProject with server_session...
+    /// let client = RemoteClient::fake_client(opts, cx).await;
+    /// ```
     #[cfg(any(test, feature = "test-support"))]
     pub fn fake_server(
         client_cx: &mut gpui::TestAppContext,
         server_cx: &mut gpui::TestAppContext,
     ) -> (RemoteConnectionOptions, AnyProtoClient) {
-        use crate::transport::ssh::SshConnectionHost;
-
-        let port = client_cx
-            .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
-        let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions {
-            host: SshConnectionHost::from("<fake>".to_string()),
-            port: Some(port),
-            ..Default::default()
-        });
-        let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
-        let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
-        let server_client = server_cx
-            .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server", false));
-        let connection: Arc<dyn RemoteConnection> = Arc::new(fake::FakeRemoteConnection {
-            connection_options: opts.clone(),
-            server_cx: fake::SendableCx::new(server_cx),
-            server_channel: server_client.clone(),
-        });
-
-        client_cx.update(|cx| {
-            cx.update_default_global(|c: &mut ConnectionPool, cx| {
-                c.connections.insert(
-                    opts.clone(),
-                    ConnectionPoolEntry::Connecting(
-                        cx.background_spawn({
-                            let connection = connection.clone();
-                            async move { Ok(connection.clone()) }
-                        })
-                        .shared(),
-                    ),
-                );
-            })
-        });
-
-        (opts, server_client.into())
+        use crate::transport::mock::MockConnection;
+        let (opts, server_client) = MockConnection::new(client_cx, server_cx);
+        (opts.into(), server_client)
     }
 
+    /// Creates a `RemoteClient` connected to a mock server.
+    ///
+    /// Call `fake_server` first to get the connection options, set up the
+    /// `HeadlessProject` with the server session, then call this method
+    /// to create the client.
     #[cfg(any(test, feature = "test-support"))]
     pub async fn fake_client(
         opts: RemoteConnectionOptions,
         client_cx: &mut gpui::TestAppContext,
     ) -> Entity<Self> {
+        use crate::transport::mock::MockDelegate;
         let (_tx, rx) = oneshot::channel();
         let mut cx = client_cx.to_async();
-        let connection = connect(opts, Arc::new(fake::Delegate), &mut cx)
+        let connection = connect(opts, Arc::new(MockDelegate), &mut cx)
             .await
             .unwrap();
         client_cx
@@ -1031,7 +1018,7 @@ impl RemoteClient {
                     ConnectionIdentifier::setup(),
                     connection,
                     rx,
-                    Arc::new(fake::Delegate),
+                    Arc::new(MockDelegate),
                     cx,
                 )
             })
@@ -1107,6 +1094,17 @@ impl ConnectionPool {
                                 .await
                                 .map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>)
                         }
+                        #[cfg(any(test, feature = "test-support"))]
+                        RemoteConnectionOptions::Mock(opts) => {
+                            cx.update(|cx| {
+                                cx.default_global::<crate::transport::mock::MockConnectionRegistry>()
+                                    .take(&opts)
+                                    .ok_or_else(|| anyhow!(
+                                        "Mock connection not found. Call MockConnection::new() first."
+                                    ))
+                                    .map(|connection| connection as Arc<dyn RemoteConnection>)
+                            })
+                        }
                     };
 
                     cx.update_global(|pool: &mut Self, _| {
@@ -1143,6 +1141,8 @@ pub enum RemoteConnectionOptions {
     Ssh(SshConnectionOptions),
     Wsl(WslConnectionOptions),
     Docker(DockerConnectionOptions),
+    #[cfg(any(test, feature = "test-support"))]
+    Mock(crate::transport::mock::MockConnectionOptions),
 }
 
 impl RemoteConnectionOptions {
@@ -1151,6 +1151,8 @@ impl RemoteConnectionOptions {
             RemoteConnectionOptions::Ssh(opts) => opts.host.to_string(),
             RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(),
             RemoteConnectionOptions::Docker(opts) => opts.name.clone(),
+            #[cfg(any(test, feature = "test-support"))]
+            RemoteConnectionOptions::Mock(opts) => format!("mock-{}", opts.id),
         }
     }
 }
@@ -1167,6 +1169,13 @@ impl From<WslConnectionOptions> for RemoteConnectionOptions {
     }
 }
 
+#[cfg(any(test, feature = "test-support"))]
+impl From<crate::transport::mock::MockConnectionOptions> for RemoteConnectionOptions {
+    fn from(opts: crate::transport::mock::MockConnectionOptions) -> Self {
+        RemoteConnectionOptions::Mock(opts)
+    }
+}
+
 #[cfg(target_os = "windows")]
 /// Open a wsl path (\\wsl.localhost\<distro>\path)
 #[derive(Debug, Clone, PartialEq, Eq, gpui::Action)]
@@ -1254,7 +1263,7 @@ impl<T: Send + Clone + 'static> Signal<T> {
     }
 }
 
-struct ChannelClient {
+pub(crate) struct ChannelClient {
     next_message_id: AtomicU32,
     outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>,
     buffer: Mutex<VecDeque<Envelope>>,
@@ -1268,7 +1277,7 @@ struct ChannelClient {
 }
 
 impl ChannelClient {
-    fn new(
+    pub(crate) fn new(
         incoming_rx: mpsc::UnboundedReceiver<Envelope>,
         outgoing_tx: mpsc::UnboundedSender<Envelope>,
         cx: &App,
@@ -1419,7 +1428,7 @@ impl ChannelClient {
         })
     }
 
-    fn reconnect(
+    pub(crate) fn reconnect(
         self: &Arc<Self>,
         incoming_rx: UnboundedReceiver<Envelope>,
         outgoing_tx: UnboundedSender<Envelope>,
@@ -1573,201 +1582,3 @@ impl ProtoClient for ChannelClient {
         self.has_wsl_interop
     }
 }
-
-#[cfg(any(test, feature = "test-support"))]
-mod fake {
-    use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform};
-    use crate::remote_client::{CommandTemplate, RemoteConnectionOptions};
-    use anyhow::Result;
-    use askpass::EncryptedPassword;
-    use async_trait::async_trait;
-    use collections::HashMap;
-    use futures::{
-        FutureExt, SinkExt, StreamExt,
-        channel::{
-            mpsc::{self, Sender},
-            oneshot,
-        },
-        select_biased,
-    };
-    use gpui::{App, AppContext as _, AsyncApp, Task, TestAppContext};
-    use release_channel::ReleaseChannel;
-    use rpc::proto::Envelope;
-    use semver::Version;
-    use std::{path::PathBuf, sync::Arc};
-    use util::paths::{PathStyle, RemotePathBuf};
-
-    pub(super) struct FakeRemoteConnection {
-        pub(super) connection_options: RemoteConnectionOptions,
-        pub(super) server_channel: Arc<ChannelClient>,
-        pub(super) server_cx: SendableCx,
-    }
-
-    pub(super) struct SendableCx(AsyncApp);
-    impl SendableCx {
-        // SAFETY: When run in test mode, GPUI is always single threaded.
-        pub(super) fn new(cx: &TestAppContext) -> Self {
-            Self(cx.to_async())
-        }
-
-        // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp
-        fn get(&self, _: &AsyncApp) -> AsyncApp {
-            self.0.clone()
-        }
-    }
-
-    // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`]
-    unsafe impl Send for SendableCx {}
-    unsafe impl Sync for SendableCx {}
-
-    #[async_trait(?Send)]
-    impl RemoteConnection for FakeRemoteConnection {
-        async fn kill(&self) -> Result<()> {
-            Ok(())
-        }
-
-        fn has_been_killed(&self) -> bool {
-            false
-        }
-
-        fn build_command(
-            &self,
-            program: Option<String>,
-            args: &[String],
-            env: &HashMap<String, String>,
-            _: Option<String>,
-            _: Option<(u16, String, u16)>,
-        ) -> Result<CommandTemplate> {
-            let ssh_program = program.unwrap_or_else(|| "sh".to_string());
-            let mut ssh_args = Vec::new();
-            ssh_args.push(ssh_program);
-            ssh_args.extend(args.iter().cloned());
-            Ok(CommandTemplate {
-                program: "ssh".into(),
-                args: ssh_args,
-                env: env.clone(),
-            })
-        }
-
-        fn build_forward_ports_command(
-            &self,
-            forwards: Vec<(u16, String, u16)>,
-        ) -> anyhow::Result<CommandTemplate> {
-            Ok(CommandTemplate {
-                program: "ssh".into(),
-                args: std::iter::once("-N".to_owned())
-                    .chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
-                        format!("{local_port}:{host}:{remote_port}")
-                    }))
-                    .collect(),
-                env: Default::default(),
-            })
-        }
-
-        fn upload_directory(
-            &self,
-            _src_path: PathBuf,
-            _dest_path: RemotePathBuf,
-            _cx: &App,
-        ) -> Task<Result<()>> {
-            unreachable!()
-        }
-
-        fn connection_options(&self) -> RemoteConnectionOptions {
-            self.connection_options.clone()
-        }
-
-        fn simulate_disconnect(&self, cx: &AsyncApp) {
-            let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
-            let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
-            self.server_channel
-                .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
-        }
-
-        fn start_proxy(
-            &self,
-            _unique_identifier: String,
-            _reconnect: bool,
-            mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
-            mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
-            mut connection_activity_tx: Sender<()>,
-            _delegate: Arc<dyn RemoteClientDelegate>,
-            cx: &mut AsyncApp,
-        ) -> Task<Result<i32>> {
-            let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
-            let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
-
-            self.server_channel.reconnect(
-                server_incoming_rx,
-                server_outgoing_tx,
-                &self.server_cx.get(cx),
-            );
-
-            cx.background_spawn(async move {
-                loop {
-                    select_biased! {
-                        server_to_client = server_outgoing_rx.next().fuse() => {
-                            let Some(server_to_client) = server_to_client else {
-                                return Ok(1)
-                            };
-                            connection_activity_tx.try_send(()).ok();
-                            client_incoming_tx.send(server_to_client).await.ok();
-                        }
-                        client_to_server = client_outgoing_rx.next().fuse() => {
-                            let Some(client_to_server) = client_to_server else {
-                                return Ok(1)
-                            };
-                            server_incoming_tx.send(client_to_server).await.ok();
-                        }
-                    }
-                }
-            })
-        }
-
-        fn path_style(&self) -> PathStyle {
-            PathStyle::local()
-        }
-
-        fn shell(&self) -> String {
-            "sh".to_owned()
-        }
-
-        fn default_system_shell(&self) -> String {
-            "sh".to_owned()
-        }
-
-        fn has_wsl_interop(&self) -> bool {
-            false
-        }
-    }
-
-    pub(super) struct Delegate;
-
-    impl RemoteClientDelegate for Delegate {
-        fn ask_password(&self, _: String, _: oneshot::Sender<EncryptedPassword>, _: &mut AsyncApp) {
-            unreachable!()
-        }
-
-        fn download_server_binary_locally(
-            &self,
-            _: RemotePlatform,
-            _: ReleaseChannel,
-            _: Option<Version>,
-            _: &mut AsyncApp,
-        ) -> Task<Result<PathBuf>> {
-            unreachable!()
-        }
-
-        fn get_download_url(
-            &self,
-            _platform: RemotePlatform,
-            _release_channel: ReleaseChannel,
-            _version: Option<Version>,
-            _cx: &mut AsyncApp,
-        ) -> Task<Result<Option<String>>> {
-            unreachable!()
-        }
-
-        fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {}
-    }
-}

crates/remote/src/transport.rs 🔗

@@ -13,6 +13,8 @@ use rpc::proto::Envelope;
 use smol::process::Child;
 
 pub mod docker;
+#[cfg(any(test, feature = "test-support"))]
+pub mod mock;
 pub mod ssh;
 pub mod wsl;
 

crates/remote/src/transport/mock.rs 🔗

@@ -0,0 +1,312 @@
+//! Mock transport for testing remote connections.

+//!

+//! This module provides a mock implementation of the `RemoteConnection` trait

+//! that allows testing remote editing functionality without actual SSH/WSL/Docker

+//! connections.

+//!

+//! # Usage

+//!

+//! ```rust,ignore

+//! use remote::{MockConnection, RemoteClient};

+//!

+//! #[gpui::test]

+//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {

+//!     let (opts, server_session) = MockConnection::new(cx, server_cx);

+//!

+//!     // Create the headless project (server side)

+//!     server_cx.update(HeadlessProject::init);

+//!     let _headless = server_cx.new(|cx| {

+//!         HeadlessProject::new(

+//!             HeadlessAppState { session: server_session, /* ... */ },

+//!             false,

+//!             cx,

+//!         )

+//!     });

+//!

+//!     // Create the client using the helper

+//!     let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await;

+//!     // ... test logic ...

+//! }

+//! ```

+

+use crate::remote_client::{

+    ChannelClient, CommandTemplate, RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,

+};

+use anyhow::Result;

+use async_trait::async_trait;

+use collections::HashMap;

+use futures::{

+    FutureExt, SinkExt, StreamExt,

+    channel::mpsc::{self, Sender},

+    select_biased,

+};

+use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext};

+use rpc::{AnyProtoClient, proto::Envelope};

+use std::{

+    path::PathBuf,

+    sync::{

+        Arc,

+        atomic::{AtomicU64, Ordering},

+    },

+};

+use util::paths::{PathStyle, RemotePathBuf};

+

+/// Unique identifier for a mock connection.

+#[derive(Debug, Clone, PartialEq, Eq, Hash)]

+pub struct MockConnectionOptions {

+    pub id: u64,

+}

+

+/// A mock implementation of `RemoteConnection` for testing.

+pub struct MockRemoteConnection {

+    options: MockConnectionOptions,

+    server_channel: Arc<ChannelClient>,

+    server_cx: SendableCx,

+}

+

+/// Wrapper to pass `AsyncApp` across thread boundaries in tests.

+///

+/// # Safety

+///

+/// This is safe because in test mode, GPUI is always single-threaded and so

+/// having access to one async app means being on the same main thread.

+pub(crate) struct SendableCx(AsyncApp);

+

+impl SendableCx {

+    pub(crate) fn new(cx: &TestAppContext) -> Self {

+        Self(cx.to_async())

+    }

+

+    pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp {

+        self.0.clone()

+    }

+}

+

+// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx

+// is only accessed from the main thread via the get() method which

+// requires a valid AsyncApp reference.

+unsafe impl Send for SendableCx {}

+unsafe impl Sync for SendableCx {}

+

+/// Global registry that holds pre-created mock connections.

+///

+/// When `ConnectionPool::connect` is called with `MockConnectionOptions`,

+/// it retrieves the connection from this registry.

+#[derive(Default)]

+pub struct MockConnectionRegistry {

+    pending: HashMap<MockConnectionOptions, Arc<MockRemoteConnection>>,

+}

+

+impl Global for MockConnectionRegistry {}

+

+impl MockConnectionRegistry {

+    /// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection.

+    pub fn take(&mut self, opts: &MockConnectionOptions) -> Option<Arc<MockRemoteConnection>> {

+        self.pending.remove(opts)

+    }

+}

+

+/// Helper for creating mock connection pairs in tests.

+pub struct MockConnection;

+

+impl MockConnection {

+    /// Creates a new mock connection pair for testing.

+    ///

+    /// This function:

+    /// 1. Creates a unique `MockConnectionOptions` identifier

+    /// 2. Sets up the server-side channel (returned as `AnyProtoClient`)

+    /// 3. Creates a `MockRemoteConnection` and registers it in the global registry

+    /// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called

+    ///

+    /// Returns:

+    /// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation

+    /// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session

+    ///

+    /// # Arguments

+    /// - `client_cx`: The test context for the client side

+    /// - `server_cx`: The test context for the server/headless side

+    pub fn new(

+        client_cx: &mut TestAppContext,

+        server_cx: &mut TestAppContext,

+    ) -> (MockConnectionOptions, AnyProtoClient) {

+        static NEXT_ID: AtomicU64 = AtomicU64::new(0);

+        let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);

+        let opts = MockConnectionOptions { id };

+

+        let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();

+        let (_, incoming_rx) = mpsc::unbounded::<Envelope>();

+        let server_client = server_cx

+            .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false));

+

+        let connection = Arc::new(MockRemoteConnection {

+            options: opts.clone(),

+            server_channel: server_client.clone(),

+            server_cx: SendableCx::new(server_cx),

+        });

+

+        client_cx.update(|cx| {

+            cx.default_global::<MockConnectionRegistry>()

+                .pending

+                .insert(opts.clone(), connection);

+        });

+

+        (opts, server_client.into())

+    }

+}

+

+#[async_trait(?Send)]

+impl RemoteConnection for MockRemoteConnection {

+    async fn kill(&self) -> Result<()> {

+        Ok(())

+    }

+

+    fn has_been_killed(&self) -> bool {

+        false

+    }

+

+    fn build_command(

+        &self,

+        program: Option<String>,

+        args: &[String],

+        env: &HashMap<String, String>,

+        _working_dir: Option<String>,

+        _port_forward: Option<(u16, String, u16)>,

+    ) -> Result<CommandTemplate> {

+        let shell_program = program.unwrap_or_else(|| "sh".to_string());

+        let mut shell_args = Vec::new();

+        shell_args.push(shell_program);

+        shell_args.extend(args.iter().cloned());

+        Ok(CommandTemplate {

+            program: "mock".into(),

+            args: shell_args,

+            env: env.clone(),

+        })

+    }

+

+    fn build_forward_ports_command(

+        &self,

+        forwards: Vec<(u16, String, u16)>,

+    ) -> Result<CommandTemplate> {

+        Ok(CommandTemplate {

+            program: "mock".into(),

+            args: std::iter::once("-N".to_owned())

+                .chain(forwards.into_iter().map(|(local_port, host, remote_port)| {

+                    format!("{local_port}:{host}:{remote_port}")

+                }))

+                .collect(),

+            env: Default::default(),

+        })

+    }

+

+    fn upload_directory(

+        &self,

+        _src_path: PathBuf,

+        _dest_path: RemotePathBuf,

+        _cx: &App,

+    ) -> Task<Result<()>> {

+        Task::ready(Ok(()))

+    }

+

+    fn connection_options(&self) -> RemoteConnectionOptions {

+        RemoteConnectionOptions::Mock(self.options.clone())

+    }

+

+    fn simulate_disconnect(&self, cx: &AsyncApp) {

+        let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();

+        let (_, incoming_rx) = mpsc::unbounded::<Envelope>();

+        self.server_channel

+            .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));

+    }

+

+    fn start_proxy(

+        &self,

+        _unique_identifier: String,

+        _reconnect: bool,

+        mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,

+        mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,

+        mut connection_activity_tx: Sender<()>,

+        _delegate: Arc<dyn RemoteClientDelegate>,

+        cx: &mut AsyncApp,

+    ) -> Task<Result<i32>> {

+        let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();

+        let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();

+

+        self.server_channel.reconnect(

+            server_incoming_rx,

+            server_outgoing_tx,

+            &self.server_cx.get(cx),

+        );

+

+        cx.background_spawn(async move {

+            loop {

+                select_biased! {

+                    server_to_client = server_outgoing_rx.next().fuse() => {

+                        let Some(server_to_client) = server_to_client else {

+                            return Ok(1)

+                        };

+                        connection_activity_tx.try_send(()).ok();

+                        client_incoming_tx.send(server_to_client).await.ok();

+                    }

+                    client_to_server = client_outgoing_rx.next().fuse() => {

+                        let Some(client_to_server) = client_to_server else {

+                            return Ok(1)

+                        };

+                        server_incoming_tx.send(client_to_server).await.ok();

+                    }

+                }

+            }

+        })

+    }

+

+    fn path_style(&self) -> PathStyle {

+        PathStyle::local()

+    }

+

+    fn shell(&self) -> String {

+        "sh".to_owned()

+    }

+

+    fn default_system_shell(&self) -> String {

+        "sh".to_owned()

+    }

+

+    fn has_wsl_interop(&self) -> bool {

+        false

+    }

+}

+

+/// Mock delegate for tests that don't need delegate functionality.

+pub struct MockDelegate;

+

+impl RemoteClientDelegate for MockDelegate {

+    fn ask_password(

+        &self,

+        _prompt: String,

+        _sender: futures::channel::oneshot::Sender<askpass::EncryptedPassword>,

+        _cx: &mut AsyncApp,

+    ) {

+        unreachable!("MockDelegate::ask_password should not be called in tests")

+    }

+

+    fn download_server_binary_locally(

+        &self,

+        _platform: crate::RemotePlatform,

+        _release_channel: release_channel::ReleaseChannel,

+        _version: Option<semver::Version>,

+        _cx: &mut AsyncApp,

+    ) -> Task<Result<PathBuf>> {

+        unreachable!("MockDelegate::download_server_binary_locally should not be called in tests")

+    }

+

+    fn get_download_url(

+        &self,

+        _platform: crate::RemotePlatform,

+        _release_channel: release_channel::ReleaseChannel,

+        _version: Option<semver::Version>,

+        _cx: &mut AsyncApp,

+    ) -> Task<Result<Option<String>>> {

+        unreachable!("MockDelegate::get_download_url should not be called in tests")

+    }

+

+    fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {}

+}

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -1894,7 +1894,7 @@ async fn test_remote_external_agent_server(
     assert_eq!(
         command,
         AgentServerCommand {
-            path: "ssh".into(),
+            path: "mock".into(),
             args: vec!["foo-cli".into(), "--flag".into()],
             env: Some(HashMap::from_iter([
                 ("VAR".into(), "val".into()),

crates/title_bar/Cargo.toml 🔗

@@ -22,8 +22,10 @@ test-support = [
     "gpui/test-support",
     "http_client/test-support",
     "project/test-support",
+    "remote/test-support",
     "util/test-support",
     "workspace/test-support",
+    "recent_projects/test-support",
 ]
 
 [dependencies]

crates/title_bar/src/title_bar.rs 🔗

@@ -366,6 +366,8 @@ impl TitleBar {
             RemoteConnectionOptions::Docker(_dev_container_connection) => {
                 (None, "Dev Container", IconName::Box)
             }
+            #[cfg(any(test, feature = "test-support"))]
+            RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
         };
 
         let nickname = nickname.unwrap_or_else(|| host.clone());

crates/workspace/src/persistence.rs 🔗

@@ -1325,6 +1325,11 @@ impl WorkspaceDb {
                 container_id = Some(options.container_id);
                 name = Some(options.name);
             }
+            #[cfg(any(test, feature = "test-support"))]
+            RemoteConnectionOptions::Mock(options) => {
+                kind = RemoteConnectionKind::Ssh;
+                host = Some(format!("mock-{}", options.id));
+            }
         }
         Self::get_or_create_remote_connection_query(
             this,

crates/zed/Cargo.toml 🔗

@@ -23,6 +23,7 @@ test-support = [
     "editor/test-support",
     "terminal_view/test-support",
     "image_viewer/test-support",
+    "recent_projects/test-support",
 ]
 visual-tests = [
     "gpui/test-support",