acp: Add e2e test support for NativeAgent (#36635)

Release Notes:

- N/A
This commit is contained in:
Ben Brandt 2025-08-21 02:36:50 +02:00 committed by Joseph T. Lyons
parent 1c91d4b17c
commit 7f0ce7c6de
8 changed files with 172 additions and 36 deletions

4
Cargo.lock generated
View file

@ -268,11 +268,14 @@ dependencies = [
"agent_settings", "agent_settings",
"agentic-coding-protocol", "agentic-coding-protocol",
"anyhow", "anyhow",
"client",
"collections", "collections",
"context_server", "context_server",
"env_logger 0.11.8", "env_logger 0.11.8",
"fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"gpui_tokio",
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
@ -284,6 +287,7 @@ dependencies = [
"paths", "paths",
"project", "project",
"rand 0.8.5", "rand 0.8.5",
"reqwest_client",
"schemars", "schemars",
"semver", "semver",
"serde", "serde",

View file

@ -10,6 +10,7 @@ path = "src/agent2.rs"
[features] [features]
test-support = ["db/test-support"] test-support = ["db/test-support"]
e2e = []
[lints] [lints]
workspace = true workspace = true
@ -72,6 +73,7 @@ zstd.workspace = true
[dev-dependencies] [dev-dependencies]
agent = { workspace = true, "features" = ["test-support"] } agent = { workspace = true, "features" = ["test-support"] }
agent_servers = { workspace = true, "features" = ["test-support"] }
assistant_context = { workspace = true, "features" = ["test-support"] } assistant_context = { workspace = true, "features" = ["test-support"] }
ctor.workspace = true ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] } client = { workspace = true, "features" = ["test-support"] }

View file

@ -73,3 +73,52 @@ impl AgentServer for NativeAgentServer {
self self
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use assistant_context::ContextStore;
use gpui::AppContext;
agent_servers::e2e_tests::common_e2e_tests!(
async |fs, project, cx| {
let auth = cx.update(|cx| {
prompt_store::init(cx);
terminal::init(cx);
let registry = language_model::LanguageModelRegistry::read_global(cx);
let auth = registry
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap()
.authenticate(cx);
cx.spawn(async move |_| auth.await)
});
auth.await.unwrap();
cx.update(|cx| {
let registry = language_model::LanguageModelRegistry::global(cx);
registry.update(cx, |registry, cx| {
registry.select_default_model(
Some(&language_model::SelectedModel {
provider: language_model::ANTHROPIC_PROVIDER_ID,
model: language_model::LanguageModelId("claude-sonnet-4-latest".into()),
}),
cx,
);
});
});
let history = cx.update(|cx| {
let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx));
cx.new(move |cx| HistoryStore::new(context_store, cx))
});
NativeAgentServer::new(fs.clone(), history)
},
allow_option_id = "allow"
);
}

View file

@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[features] [features]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
e2e = [] e2e = []
[lints] [lints]
@ -23,10 +23,14 @@ agent-client-protocol.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
agentic-coding-protocol.workspace = true agentic-coding-protocol.workspace = true
anyhow.workspace = true anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true collections.workspace = true
context_server.workspace = true context_server.workspace = true
env_logger = { workspace = true, optional = true }
fs = { workspace = true, optional = true }
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
indoc.workspace = true indoc.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
@ -36,6 +40,7 @@ log.workspace = true
paths.workspace = true paths.workspace = true
project.workspace = true project.workspace = true
rand.workspace = true rand.workspace = true
reqwest_client = { workspace = true, optional = true }
schemars.workspace = true schemars.workspace = true
semver.workspace = true semver.workspace = true
serde.workspace = true serde.workspace = true
@ -57,8 +62,12 @@ libc.workspace = true
nix.workspace = true nix.workspace = true
[dev-dependencies] [dev-dependencies]
client = { workspace = true, features = ["test-support"] }
env_logger.workspace = true env_logger.workspace = true
fs.workspace = true
language.workspace = true language.workspace = true
indoc.workspace = true indoc.workspace = true
acp_thread = { workspace = true, features = ["test-support"] } acp_thread = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
gpui_tokio.workspace = true
reqwest_client = { workspace = true, features = ["test-support"] }

View file

@ -3,8 +3,8 @@ mod claude;
mod gemini; mod gemini;
mod settings; mod settings;
#[cfg(test)] #[cfg(any(test, feature = "test-support"))]
mod e2e_tests; pub mod e2e_tests;
pub use claude::*; pub use claude::*;
pub use gemini::*; pub use gemini::*;

View file

@ -1093,7 +1093,7 @@ pub(crate) mod tests {
use gpui::TestAppContext; use gpui::TestAppContext;
use serde_json::json; use serde_json::json;
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow");
pub fn local_command() -> AgentServerCommand { pub fn local_command() -> AgentServerCommand {
AgentServerCommand { AgentServerCommand {

View file

@ -4,21 +4,30 @@ use std::{
time::Duration, time::Duration,
}; };
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings}; use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select}; use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{Entity, TestAppContext}; use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc; use indoc::indoc;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use settings::{Settings, SettingsStore};
use util::path; use util::path;
pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
let fs = init_test(cx).await; where
let project = Project::test(fs, [], cx).await; T: AgentServer + 'static,
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs.clone(), [], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
thread thread
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
@ -42,8 +51,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
}); });
} }
pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
let _fs = init_test(cx).await; where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
std::fs::write( std::fs::write(
@ -56,7 +69,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
) )
.expect("failed to write file"); .expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await; let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await; let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
tempdir.path(),
cx,
)
.await;
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.send( thread.send(
@ -110,15 +129,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
drop(tempdir); drop(tempdir);
} }
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
let _fs = init_test(cx).await; where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let foo_path = tempdir.path().join("foo"); let foo_path = tempdir.path().join("foo");
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await; let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
@ -152,14 +181,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
drop(tempdir); drop(tempdir);
} }
pub async fn test_tool_call_with_permission( pub async fn test_tool_call_with_permission<T, F>(
server: impl AgentServer + 'static, server: F,
allow_option_id: acp::PermissionOptionId, allow_option_id: acp::PermissionOptionId,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) where
let fs = init_test(cx).await; T: AgentServer + 'static,
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; {
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
let full_turn = thread.update(cx, |thread, cx| { let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw( thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@ -247,11 +285,21 @@ pub async fn test_tool_call_with_permission(
}); });
} }
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
let fs = init_test(cx).await; where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
let _ = thread.update(cx, |thread, cx| { let _ = thread.update(cx, |thread, cx| {
thread.send_raw( thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@ -316,10 +364,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
}); });
} }
pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) { pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
let fs = init_test(cx).await; where
let project = Project::test(fs, [], cx).await; T: AgentServer + 'static,
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
let project = Project::test(fs.clone(), [], cx).await;
let thread = new_test_thread(
server(&fs, &project, cx).await,
project.clone(),
"/private/tmp",
cx,
)
.await;
thread thread
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
@ -386,25 +444,39 @@ macro_rules! common_e2e_tests {
} }
}; };
} }
pub use common_e2e_tests;
// Helpers // Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> { pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
use settings::Settings;
env_logger::try_init().ok(); env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
Project::init_settings(cx); Project::init_settings(cx);
language::init(cx); language::init(cx);
gpui_tokio::init(cx);
let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client, cx);
agent_settings::init(cx);
crate::settings::init(cx); crate::settings::init(cx);
#[cfg(test)]
crate::AllAgentServersSettings::override_global( crate::AllAgentServersSettings::override_global(
AllAgentServersSettings { crate::AllAgentServersSettings {
claude: Some(AgentServerSettings { claude: Some(crate::AgentServerSettings {
command: crate::claude::tests::local_command(), command: crate::claude::tests::local_command(),
}), }),
gemini: Some(AgentServerSettings { gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(), command: crate::gemini::tests::local_command(),
}), }),
}, },

View file

@ -108,7 +108,7 @@ pub(crate) mod tests {
use crate::AgentServerCommand; use crate::AgentServerCommand;
use std::path::Path; use std::path::Path;
crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
pub fn local_command() -> AgentServerCommand { pub fn local_command() -> AgentServerCommand {
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))