acp: Add e2e test support for NativeAgent (#36635)
Release Notes: - N/A
This commit is contained in:
parent
6f242772cc
commit
568e1d0a42
8 changed files with 172 additions and 36 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue