test: add ACP integration test (#6150)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Generated
+6
-5
@@ -30,9 +30,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.9.0"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -47,9 +47,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol-schema"
|
name = "agent-client-protocol-schema"
|
||||||
version = "0.10.2"
|
version = "0.10.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50cbb9a55080793c01e5e97c6fb0f1032d1f2460f7e3869225813cfa29001585"
|
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
@@ -2308,7 +2308,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3069,6 +3069,7 @@ dependencies = [
|
|||||||
name = "goose"
|
name = "goose"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agent-client-protocol",
|
||||||
"ahash",
|
"ahash",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|||||||
@@ -119,11 +119,13 @@ boa_gc = "0.21"
|
|||||||
winapi = { version = "0.3", features = ["wincred"] }
|
winapi = { version = "0.3", features = ["wincred"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
agent-client-protocol = "0.9.2"
|
||||||
criterion = "0.5"
|
criterion = "0.5"
|
||||||
serial_test = "3.2.0"
|
serial_test = "3.2.0"
|
||||||
mockall = "0.13.1"
|
mockall = "0.13.1"
|
||||||
wiremock = "0.6.0"
|
wiremock = "0.6.0"
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7.15", features = ["compat"] }
|
||||||
temp-env = "0.3.6"
|
temp-env = "0.3.6"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
ctor = "0.2.9"
|
ctor = "0.2.9"
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
use agent_client_protocol::{
|
||||||
|
self as acp, Agent, Client, ClientSideConnection, ContentBlock, InitializeRequest,
|
||||||
|
NewSessionRequest, PromptRequest, ProtocolVersion, SessionNotification, SessionUpdate,
|
||||||
|
TextContent,
|
||||||
|
};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::process::{Child, Command};
|
||||||
|
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
|
||||||
|
use wiremock::matchers::{method, path};
|
||||||
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||||
|
|
||||||
|
const BASIC_RESPONSE: &str = include_str!("./test_data/openai_chat_completion_streaming.txt");
|
||||||
|
const BASIC_TEXT: &str = "Hello! How can I assist you today? 🌍";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_acp_basic_completion() {
|
||||||
|
let mock_server = setup_mock_openai(BASIC_RESPONSE).await;
|
||||||
|
let work_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let (client, updates) = TestClient::new();
|
||||||
|
let child = spawn_goose_acp(&mock_server).await;
|
||||||
|
|
||||||
|
run_acp_session(
|
||||||
|
client,
|
||||||
|
child,
|
||||||
|
work_dir.path(),
|
||||||
|
|conn, session_id| async move {
|
||||||
|
let response = conn
|
||||||
|
.prompt(PromptRequest::new(
|
||||||
|
session_id,
|
||||||
|
vec![ContentBlock::Text(TextContent::new("test message"))],
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.stop_reason, acp::StopReason::EndTurn);
|
||||||
|
|
||||||
|
wait_for_text(&updates, BASIC_TEXT, Duration::from_secs(5)).await;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_text(
|
||||||
|
updates: &Arc<Mutex<Vec<SessionNotification>>>,
|
||||||
|
expected: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
) {
|
||||||
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
|
loop {
|
||||||
|
let actual = extract_text(&updates.lock().unwrap());
|
||||||
|
if actual == expected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if tokio::time::Instant::now() > deadline {
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_mock_openai(streaming_response: &str) -> MockServer {
|
||||||
|
let mock_server = MockServer::start().await;
|
||||||
|
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/chat/completions"))
|
||||||
|
.respond_with(
|
||||||
|
ResponseTemplate::new(200)
|
||||||
|
.insert_header("content-type", "text/event-stream")
|
||||||
|
.set_body_string(streaming_response),
|
||||||
|
)
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mock_server
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_text(updates: &[SessionNotification]) -> String {
|
||||||
|
updates
|
||||||
|
.iter()
|
||||||
|
.filter_map(|n| match &n.update {
|
||||||
|
SessionUpdate::AgentMessageChunk(chunk) => match &chunk.content {
|
||||||
|
ContentBlock::Text(t) => Some(t.text.clone()),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestClient {
|
||||||
|
updates: Arc<Mutex<Vec<SessionNotification>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestClient {
|
||||||
|
fn new() -> (Self, Arc<Mutex<Vec<SessionNotification>>>) {
|
||||||
|
let updates = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
updates: updates.clone(),
|
||||||
|
},
|
||||||
|
updates,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl Client for TestClient {
|
||||||
|
async fn request_permission(
|
||||||
|
&self,
|
||||||
|
_args: acp::RequestPermissionRequest,
|
||||||
|
) -> acp::Result<acp::RequestPermissionResponse> {
|
||||||
|
Err(acp::Error::method_not_found())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn session_notification(&self, args: SessionNotification) -> acp::Result<()> {
|
||||||
|
self.updates.lock().unwrap().push(args);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_goose_acp(mock_server: &MockServer) -> Child {
|
||||||
|
Command::new("cargo")
|
||||||
|
.args(["run", "-p", "goose-cli", "--", "acp"])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.env("GOOSE_PROVIDER", "openai")
|
||||||
|
.env("GOOSE_MODEL", "gpt-5-nano")
|
||||||
|
.env("OPENAI_HOST", mock_server.uri())
|
||||||
|
.env("OPENAI_API_KEY", "test-key")
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_acp_session<F, Fut>(client: TestClient, mut child: Child, work_dir: &Path, test_fn: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(ClientSideConnection, acp::SessionId) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = ()>,
|
||||||
|
{
|
||||||
|
let outgoing = child.stdin.take().unwrap().compat_write();
|
||||||
|
let incoming = child.stdout.take().unwrap().compat();
|
||||||
|
|
||||||
|
let work_dir = work_dir.to_path_buf();
|
||||||
|
let local_set = tokio::task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
let (conn, handle_io) = ClientSideConnection::new(client, outgoing, incoming, |fut| {
|
||||||
|
tokio::task::spawn_local(fut);
|
||||||
|
});
|
||||||
|
tokio::task::spawn_local(handle_io);
|
||||||
|
|
||||||
|
conn.initialize(InitializeRequest::new(ProtocolVersion::V1))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let session = conn
|
||||||
|
.new_session(NewSessionRequest::new(&work_dir))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
test_fn(conn, session.session_id).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
data: {"choices":[],"created":0,"id":"","prompt_filter_results":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"prompt_index":0}]}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"","role":"assistant"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"Hello"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"!"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" How"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" can"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" I"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" assist"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" you"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" today"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"?"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" 🌍"}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: {"choices":[{"finish_reason":"stop","index":0,"content_filter_offsets":{"check_offset":3458,"start_offset":3458,"end_offset":3494},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":null}}],"created":1747592466,"id":"chatcmpl-BYcvCkaKJjQIM7e2j6vg08RIcY8qp","usage":{"completion_tokens":13,"completion_tokens_details":{"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":1675,"prompt_tokens_details":{"cached_tokens":1536},"total_tokens":1688},"model":"gpt-4o-2024-11-20","system_fingerprint":"fp_ee1d74bde0"}
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
Reference in New Issue
Block a user