use crate::{AgentServer, AgentServerDelegate};use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};use agent_client_protocol::schema::v1 as acp;use client::RefreshLlmTokenListener;use futures::{FutureExt, StreamExt, channel::mpsc, select};use gpui::AppContext;use gpui::{Entity, TestAppContext};use indoc::indoc;use project::{FakeFs, Project};#[cfg(test)]use settings::Settings;use std::{ path::{Path, PathBuf}, sync::Arc, time::Duration,};use util::path;use util::path_list::PathList; pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)where T: AgentServer + 'static, F: AsyncFn(&Arc<dyn fs::Fs>, &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, cx).await, project.clone(), "/private/tmp", cx).await; thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) .await .unwrap(); thread.read_with(cx, |thread, _| { assert!( thread.entries().len() >= 2, "Expected at least 2 entries. Got: {:?}", thread.entries() ); assert!(matches!( thread.entries()[0], AgentThreadEntry::UserMessage(_) )); assert!(matches!( thread.entries()[1], AgentThreadEntry::AssistantMessage(_) )); });} pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)where T: AgentServer + 'static, F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,{ let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); std::fs::write( tempdir.path().join("foo.rs"), indoc! {" fn main() { println!(\"Hello, world!\"); } "}, ) .expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; let thread = new_test_thread(server(&fs, cx).await, project.clone(), tempdir.path(), cx).await; thread .update(cx, |thread, cx| { thread.send( vec![ "Read the file ".into(), acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")), " and tell me what the content of the println! is".into(), ], cx, ) }) .await .unwrap(); thread.read_with(cx, |thread, cx| { assert!(matches!( thread.entries()[0], AgentThreadEntry::UserMessage(_) )); let assistant_message = &thread .entries() .iter() .rev() .find_map(|entry| match entry { AgentThreadEntry::AssistantMessage(msg) => Some(msg), _ => None, }) .unwrap(); assert!( assistant_message.to_markdown(cx).contains("Hello, world!"), "unexpected assistant message: {:?}", assistant_message.to_markdown(cx) ); }); drop(tempdir);} pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)where T: AgentServer + 'static, F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,{ let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); let foo_path = tempdir.path().join("foo"); 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 thread = new_test_thread(server(&fs, cx).await, project.clone(), "/private/tmp", cx).await; thread .update(cx, |thread, cx| { thread.send_raw( &format!("Read {} and tell me what you see.", foo_path.display()), cx, ) }) .await .unwrap(); thread.read_with(cx, |thread, _cx| { assert!(thread.entries().iter().any(|entry| { matches!( entry, AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Pending | ToolCallStatus::InProgress | ToolCallStatus::Completed, .. }) ) })); assert!( thread .entries() .iter() .any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) }) ); }); drop(tempdir);} pub async fn test_tool_call_with_permission<T, F>( server: F, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext,) where T: AgentServer + 'static, F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,{ 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, cx).await, project.clone(), "/private/tmp", cx).await; let full_turn = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, cx, ) }); run_until_first_tool_call( &thread, |entry| { matches!( entry, AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) ) }, cx, ) .await; let tool_call_id = thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { id, label, status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) = &thread .entries() .iter() .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) .unwrap() else { panic!(); }; let label = label.read(cx).source(); assert!(label.contains("touch"), "Got: {}", label); id.clone() }); thread.update(cx, |thread, cx| { thread.authorize_tool_call( tool_call_id, acp_thread::SelectedPermissionOutcome::new( allow_option_id, acp::PermissionOptionKind::AllowOnce, ), cx, ); assert!(thread.entries().iter().any(|entry| matches!( entry, AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Pending | ToolCallStatus::InProgress | ToolCallStatus::Completed, .. }) ))); }); full_turn.await.unwrap(); thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { content, status: ToolCallStatus::Pending | ToolCallStatus::InProgress | ToolCallStatus::Completed, .. }) = thread .entries() .iter() .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) .unwrap() else { panic!(); }; assert!( content.iter().any(|c| c.to_markdown(cx).contains("Hello")), "Expected content to contain 'Hello'" ); });} pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)where T: AgentServer + 'static, F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,{ 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, cx).await, project.clone(), "/private/tmp", cx).await; let _ = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, cx, ) }); let first_tool_call_ix = run_until_first_tool_call( &thread, |entry| { matches!( entry, AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) ) }, cx, ) .await; thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { id, label, status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) = &thread.entries()[first_tool_call_ix] else { panic!("{:?}", thread.entries()[1]); }; let label = label.read(cx).source(); assert!(label.contains("touch"), "Got: {}", label); id.clone() }); thread.update(cx, |thread, cx| thread.cancel(cx)).await; thread.read_with(cx, |thread, _cx| { let AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Canceled, .. }) = &thread.entries()[first_tool_call_ix] else { panic!(); }; }); thread .update(cx, |thread, cx| { thread.send_raw(r#"Stop running and say goodbye to me."#, cx) }) .await .unwrap(); thread.read_with(cx, |thread, _| { assert!(matches!( &thread.entries().last().unwrap(), AgentThreadEntry::AssistantMessage(..), )) });} pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)where T: AgentServer + 'static, F: AsyncFn(&Arc<dyn fs::Fs>, &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, cx).await, project.clone(), "/private/tmp", cx).await; thread .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) .await .unwrap(); thread.read_with(cx, |thread, _| { assert!(thread.entries().len() >= 2, "Expected at least 2 entries"); }); let weak_thread = thread.downgrade(); drop(thread); cx.executor().run_until_parked(); assert!(!weak_thread.is_upgradable());} #[macro_export]macro_rules! common_e2e_tests { ($server:expr, allow_option_id = $allow_option_id:expr) => { mod common_e2e { use super::*; #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn basic(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_basic($server, cx).await; } #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn path_mentions(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_path_mentions($server, cx).await; } #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn tool_call(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_tool_call($server, cx).await; } #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_tool_call_with_permission( $server, ::agent_client_protocol::schema::v1::PermissionOptionId::new($allow_option_id), cx, ) .await; } #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn cancel(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_cancel($server, cx).await; } #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn thread_drop(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_thread_drop($server, cx).await; } } };}pub use common_e2e_tests; // Helpers pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> { env_logger::try_init().ok(); cx.update(|cx| { let settings_store = settings::SettingsStore::test(cx); cx.set_global(settings_store); gpui_tokio::init(cx); let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); cx.set_http_client(Arc::new(http_client)); let client = client::Client::production(cx); let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx)); language_model::init(cx); RefreshLlmTokenListener::register(client.clone(), user_store, cx); #[cfg(test)] project::agent_server_store::AllAgentServersSettings::override_global( project::agent_server_store::AllAgentServersSettings(collections::HashMap::default()), cx, ); }); cx.executor().allow_parking(); FakeFs::new(cx.executor())} pub async fn new_test_thread( server: impl AgentServer + 'static, project: Entity<Project>, current_dir: impl AsRef<Path>, cx: &mut TestAppContext,) -> Entity<AcpThread> { let store = project.read_with(cx, |project, _| project.agent_server_store().clone()); let delegate = AgentServerDelegate::new(store, None, None); let connection = cx .update(|cx| server.connect(delegate, project.clone(), cx)) .await .unwrap(); cx.update(|cx| { connection.new_session(project.clone(), PathList::new(&[current_dir.as_ref()]), cx) }) .await .unwrap()} pub async fn run_until_first_tool_call( thread: &Entity<AcpThread>, wait_until: impl Fn(&AgentThreadEntry) -> bool + 'static, cx: &mut TestAppContext,) -> usize { let (mut tx, mut rx) = mpsc::channel::<usize>(1); let subscription = cx.update(|cx| { cx.subscribe(thread, move |thread, _, cx| { for (ix, entry) in thread.read(cx).entries().iter().enumerate() { if wait_until(entry) { return tx.try_send(ix).unwrap(); } } }) }); select! { _ = futures::FutureExt::fuse(cx.background_executor.timer(Duration::from_secs(20))) => { panic!("Timeout waiting for tool call") } ix = rx.next().fuse() => { drop(subscription); ix.unwrap() } }} pub fn get_zed_path() -> PathBuf { let mut zed_path = std::env::current_exe().unwrap(); while zed_path .file_name() .is_none_or(|name| name.to_string_lossy() != "debug") { if !zed_path.pop() { panic!("Could not find target directory"); } } zed_path.push("zed"); if !zed_path.exists() { panic!("\n🚨 Run `cargo build` at least once before running e2e tests\n\n"); } zed_path}
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol::schema::v1 as acp;
use client::RefreshLlmTokenListener;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::AppContext;
use gpui::{Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
#[cfg(test)]
use settings::Settings;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use util::path;
use util::path_list::PathList;
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &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, cx).await, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
.await
.unwrap();
thread.read_with(cx, |thread, _| {
assert!(
thread.entries().len() >= 2,
"Expected at least 2 entries. Got: {:?}",
thread.entries()
);
assert!(matches!(
thread.entries()[0],
AgentThreadEntry::UserMessage(_)
));
assert!(matches!(
thread.entries()[1],
AgentThreadEntry::AssistantMessage(_)
));
});
}
pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap();
std::fs::write(
tempdir.path().join("foo.rs"),
indoc! {"
fn main() {
println!(\"Hello, world!\");
}
"},
)
.expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server(&fs, cx).await, project.clone(), tempdir.path(), cx).await;
thread
.update(cx, |thread, cx| {
thread.send(
vec![
"Read the file ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")),
" and tell me what the content of the println! is".into(),
],
cx,
)
})
.await
.unwrap();
thread.read_with(cx, |thread, cx| {
assert!(matches!(
thread.entries()[0],
AgentThreadEntry::UserMessage(_)
));
let assistant_message = &thread
.entries()
.iter()
.rev()
.find_map(|entry| match entry {
AgentThreadEntry::AssistantMessage(msg) => Some(msg),
_ => None,
})
.unwrap();
assert!(
assistant_message.to_markdown(cx).contains("Hello, world!"),
"unexpected assistant message: {:?}",
assistant_message.to_markdown(cx)
);
});
drop(tempdir);
}
pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,
{
let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap();
let foo_path = tempdir.path().join("foo");
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 thread = new_test_thread(server(&fs, cx).await, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| {
thread.send_raw(
&format!("Read {} and tell me what you see.", foo_path.display()),
cx,
)
})
.await
.unwrap();
thread.read_with(cx, |thread, _cx| {
assert!(thread.entries().iter().any(|entry| {
matches!(
entry,
AgentThreadEntry::ToolCall(ToolCall {
status: ToolCallStatus::Pending
| ToolCallStatus::InProgress
| ToolCallStatus::Completed,
..
})
)
}));
assert!(
thread
.entries()
.iter()
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
);
});
drop(tempdir);
}
pub async fn test_tool_call_with_permission<T, F>(
server: F,
allow_option_id: acp::PermissionOptionId,
cx: &mut TestAppContext,
) where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,
{
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, cx).await, project.clone(), "/private/tmp", cx).await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
cx,
)
});
run_until_first_tool_call(
&thread,
|entry| {
matches!(
entry,
AgentThreadEntry::ToolCall(ToolCall {
status: ToolCallStatus::WaitingForConfirmation { .. },
..
})
)
},
cx,
)
.await;
let tool_call_id = thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
label,
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread
.entries()
.iter()
.find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_)))
.unwrap()
else {
panic!();
};
let label = label.read(cx).source();
assert!(label.contains("touch"), "Got: {}", label);
id.clone()
});
thread.update(cx, |thread, cx| {
thread.authorize_tool_call(
tool_call_id,
acp_thread::SelectedPermissionOutcome::new(
allow_option_id,
acp::PermissionOptionKind::AllowOnce,
),
cx,
);
assert!(thread.entries().iter().any(|entry| matches!(
entry,
AgentThreadEntry::ToolCall(ToolCall {
status: ToolCallStatus::Pending
| ToolCallStatus::InProgress
| ToolCallStatus::Completed,
..
})
)));
});
full_turn.await.unwrap();
thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
content,
status: ToolCallStatus::Pending
| ToolCallStatus::InProgress
| ToolCallStatus::Completed,
..
}) = thread
.entries()
.iter()
.find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_)))
.unwrap()
else {
panic!();
};
assert!(
content.iter().any(|c| c.to_markdown(cx).contains("Hello")),
"Expected content to contain 'Hello'"
);
});
}
pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &mut TestAppContext) -> T,
{
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, cx).await, project.clone(), "/private/tmp", cx).await;
let _ = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
cx,
)
});
let first_tool_call_ix = run_until_first_tool_call(
&thread,
|entry| {
matches!(
entry,
AgentThreadEntry::ToolCall(ToolCall {
status: ToolCallStatus::WaitingForConfirmation { .. },
..
})
)
},
cx,
)
.await;
thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
label,
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread.entries()[first_tool_call_ix]
else {
panic!("{:?}", thread.entries()[1]);
};
let label = label.read(cx).source();
assert!(label.contains("touch"), "Got: {}", label);
id.clone()
});
thread.update(cx, |thread, cx| thread.cancel(cx)).await;
thread.read_with(cx, |thread, _cx| {
let AgentThreadEntry::ToolCall(ToolCall {
status: ToolCallStatus::Canceled,
..
}) = &thread.entries()[first_tool_call_ix]
else {
panic!();
};
});
thread
.update(cx, |thread, cx| {
thread.send_raw(r#"Stop running and say goodbye to me."#, cx)
})
.await
.unwrap();
thread.read_with(cx, |thread, _| {
assert!(matches!(
&thread.entries().last().unwrap(),
AgentThreadEntry::AssistantMessage(..),
))
});
}
pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
where
T: AgentServer + 'static,
F: AsyncFn(&Arc<dyn fs::Fs>, &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, cx).await, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
.await
.unwrap();
thread.read_with(cx, |thread, _| {
assert!(thread.entries().len() >= 2, "Expected at least 2 entries");
});
let weak_thread = thread.downgrade();
drop(thread);
cx.executor().run_until_parked();
assert!(!weak_thread.is_upgradable());
}
#[macro_export]
macro_rules! common_e2e_tests {
($server:expr, allow_option_id = $allow_option_id:expr) => {
mod common_e2e {
use super::*;
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn basic(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_basic($server, cx).await;
}
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn path_mentions(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_path_mentions($server, cx).await;
}
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn tool_call(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_tool_call($server, cx).await;
}
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_tool_call_with_permission(
$server,
::agent_client_protocol::schema::v1::PermissionOptionId::new($allow_option_id),
cx,
)
.await;
}
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn cancel(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_cancel($server, cx).await;
}
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn thread_drop(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_thread_drop($server, cx).await;
}
}
};
}
pub use common_e2e_tests;
// Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store);
gpui_tokio::init(cx);
let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
language_model::init(cx);
RefreshLlmTokenListener::register(client.clone(), user_store, cx);
#[cfg(test)]
project::agent_server_store::AllAgentServersSettings::override_global(
project::agent_server_store::AllAgentServersSettings(collections::HashMap::default()),
cx,
);
});
cx.executor().allow_parking();
FakeFs::new(cx.executor())
}
pub async fn new_test_thread(
server: impl AgentServer + 'static,
project: Entity<Project>,
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
let delegate = AgentServerDelegate::new(store, None, None);
let connection = cx
.update(|cx| server.connect(delegate, project.clone(), cx))
.await
.unwrap();
cx.update(|cx| {
connection.new_session(project.clone(), PathList::new(&[current_dir.as_ref()]), cx)
})
.await
.unwrap()
}
pub async fn run_until_first_tool_call(
thread: &Entity<AcpThread>,
wait_until: impl Fn(&AgentThreadEntry) -> bool + 'static,
cx: &mut TestAppContext,
) -> usize {
let (mut tx, mut rx) = mpsc::channel::<usize>(1);
let subscription = cx.update(|cx| {
cx.subscribe(thread, move |thread, _, cx| {
for (ix, entry) in thread.read(cx).entries().iter().enumerate() {
if wait_until(entry) {
return tx.try_send(ix).unwrap();
}
}
})
});
select! {
_ = futures::FutureExt::fuse(cx.background_executor.timer(Duration::from_secs(20))) => {
panic!("Timeout waiting for tool call")
}
ix = rx.next().fuse() => {
drop(subscription);
ix.unwrap()
}
}
}
pub fn get_zed_path() -> PathBuf {
let mut zed_path = std::env::current_exe().unwrap();
while zed_path
.file_name()
.is_none_or(|name| name.to_string_lossy() != "debug")
{
if !zed_path.pop() {
panic!("Could not find target directory");
}
}
zed_path.push("zed");
if !zed_path.exists() {
panic!("\n🚨 Run `cargo build` at least once before running e2e tests\n\n");
}
zed_path
}