acp: Add a basic test for ACP remoting (#38381)

Cole Miller created

Tests that the downstream project can see custom agents configured in
the remote server's settings, and that it constructs an appropriate
`AgentServerCommand`.

Release Notes:

- N/A

Change summary

Cargo.lock                                       |  2 
crates/remote_server/Cargo.toml                  |  2 
crates/remote_server/src/remote_editing_tests.rs | 88 +++++++++++++++++
crates/settings/src/settings_store.rs            | 11 ++
4 files changed, 102 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -13981,6 +13981,7 @@ dependencies = [
  "clap",
  "client",
  "clock",
+ "collections",
  "crash-handler",
  "crashes",
  "dap",
@@ -14009,6 +14010,7 @@ dependencies = [
  "minidumper",
  "node_runtime",
  "paths",
+ "pretty_assertions",
  "project",
  "proto",
  "release_channel",

crates/remote_server/Cargo.toml 🔗

@@ -77,6 +77,7 @@ assistant_tool.workspace = true
 assistant_tools.workspace = true
 client = { workspace = true, features = ["test-support"] }
 clock = { workspace = true, features = ["test-support"] }
+collections.workspace = true
 dap = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }
@@ -85,6 +86,7 @@ gpui = { workspace = true, features = ["test-support"] }
 http_client = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
 node_runtime = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
 project = { workspace = true, features = ["test-support"] }
 remote = { workspace = true, features = ["test-support"] }
 language_model = { workspace = true, features = ["test-support"] }

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -6,6 +6,7 @@ use assistant_tool::{Tool as _, ToolResultContent};
 use assistant_tools::{ReadFileTool, ReadFileToolInput};
 use client::{Client, UserStore};
 use clock::FakeSystemClock;
+use collections::{HashMap, HashSet};
 use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel};
 
 use extension::ExtensionHostProxy;
@@ -20,6 +21,7 @@ use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, Language
 use node_runtime::NodeRuntime;
 use project::{
     Project, ProjectPath,
+    agent_server_store::AgentServerCommand,
     search::{SearchQuery, SearchResult},
 };
 use remote::RemoteClient;
@@ -27,7 +29,6 @@ use serde_json::json;
 use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
 use smol::stream::StreamExt;
 use std::{
-    collections::HashSet,
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -1770,6 +1771,91 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu
     does_not_exist_result.output.await.unwrap_err();
 }
 
+#[gpui::test]
+async fn test_remote_external_agent_server(
+    cx: &mut TestAppContext,
+    server_cx: &mut TestAppContext,
+) {
+    let fs = FakeFs::new(server_cx.executor());
+    fs.insert_tree(path!("/project"), json!({})).await;
+
+    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
+    project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree(path!("/project"), true, cx)
+        })
+        .await
+        .unwrap();
+    let names = project.update(cx, |project, cx| {
+        project
+            .agent_server_store()
+            .read(cx)
+            .external_agents()
+            .map(|name| name.to_string())
+            .collect::<Vec<_>>()
+    });
+    pretty_assertions::assert_eq!(names, ["gemini", "claude"]);
+    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
+        settings_store
+            .set_raw_server_settings(
+                Some(json!({
+                    "agent_servers": {
+                        "foo": {
+                            "command": "foo-cli",
+                            "args": ["--flag"],
+                            "env": {
+                                "VAR": "val"
+                            }
+                        }
+                    }
+                })),
+                cx,
+            )
+            .unwrap();
+    });
+    server_cx.run_until_parked();
+    cx.run_until_parked();
+    let names = project.update(cx, |project, cx| {
+        project
+            .agent_server_store()
+            .read(cx)
+            .external_agents()
+            .map(|name| name.to_string())
+            .collect::<Vec<_>>()
+    });
+    pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]);
+    let (command, root, login) = project
+        .update(cx, |project, cx| {
+            project.agent_server_store().update(cx, |store, cx| {
+                store
+                    .get_external_agent(&"foo".into())
+                    .unwrap()
+                    .get_command(
+                        None,
+                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
+                        None,
+                        None,
+                        &mut cx.to_async(),
+                    )
+            })
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        command,
+        AgentServerCommand {
+            path: "ssh".into(),
+            args: vec!["foo-cli".into(), "--flag".into()],
+            env: Some(HashMap::from_iter([
+                ("VAR".into(), "val".into()),
+                ("OTHER_VAR".into(), "other-val".into())
+            ]))
+        }
+    );
+    assert_eq!(&PathBuf::from(root), paths::home_dir());
+    assert!(login.is_none());
+}
+
 pub async fn init_test(
     server_fs: &Arc<FakeFs>,
     cx: &mut TestAppContext,

crates/settings/src/settings_store.rs 🔗

@@ -487,6 +487,17 @@ impl SettingsStore {
         Ok(())
     }
 
+    /// Replaces current settings with the values from the given JSON.
+    pub fn set_raw_server_settings(
+        &mut self,
+        new_settings: Option<Value>,
+        cx: &mut App,
+    ) -> Result<()> {
+        self.raw_server_settings = new_settings;
+        self.recompute_values(None, cx)?;
+        Ok(())
+    }
+
     /// Get the configured settings profile names.
     pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
         self.raw_user_settings