mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.9.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
||||
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -47,9 +47,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.10.2"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cbb9a55080793c01e5e97c6fb0f1032d1f2460f7e3869225813cfa29001585"
|
||||
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more",
|
||||
@@ -2308,7 +2308,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3069,6 +3069,7 @@ dependencies = [
|
||||
name = "goose"
|
||||
version = "1.16.0"
|
||||
dependencies = [
|
||||
"agent-client-protocol",
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -119,11 +119,13 @@ boa_gc = "0.21"
|
||||
winapi = { version = "0.3", features = ["wincred"] }
|
||||
|
||||
[dev-dependencies]
|
||||
agent-client-protocol = "0.9.2"
|
||||
criterion = "0.5"
|
||||
serial_test = "3.2.0"
|
||||
mockall = "0.13.1"
|
||||
wiremock = "0.6.0"
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
tokio-util = { version = "0.7.15", features = ["compat"] }
|
||||
temp-env = "0.3.6"
|
||||
dotenvy = "0.15.7"
|
||||
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