Skip to content

Commit 48ce0ac

Browse files
nikomatsakisclaude
andcommitted
feat(acp): add testing infrastructure
Add comprehensive testing infrastructure for ACP server development: - Mock LLM system with bidirectional channel-based communication - Enables deterministic testing without real backend calls - Supports scripted conversation flows and response validation - Actor-based design for clean async testing - ACP client test utility for integration testing - Simple ACP client that can connect to the server - Validates end-to-end protocol functionality - Useful for manual testing and automated integration tests This infrastructure enables reliable development and testing of the ACP server implementation without dependencies on external services. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 52f81e8 commit 48ce0ac

File tree

3 files changed

+425
-0
lines changed

3 files changed

+425
-0
lines changed

acp-client-test/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "acp-client-test"
3+
authors.workspace = true
4+
edition.workspace = true
5+
homepage.workspace = true
6+
publish.workspace = true
7+
version.workspace = true
8+
license.workspace = true
9+
10+
[dependencies]
11+
agent-client-protocol = { workspace = true }
12+
tokio = { version = "1.0", features = ["full"] }
13+
tokio-util = { version = "0.7", features = ["compat"] }
14+
anyhow = "1.0"
15+
serde_json = "1.0"
16+
17+
[lints]
18+
workspace = true

acp-client-test/src/main.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use std::sync::Arc;
2+
use agent_client_protocol as acp;
3+
use anyhow::Result;
4+
use serde_json::value::RawValue;
5+
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
6+
7+
struct SimpleClient;
8+
9+
impl acp::Client for SimpleClient {
10+
async fn request_permission(
11+
&self,
12+
args: acp::RequestPermissionRequest,
13+
) -> Result<acp::RequestPermissionResponse, acp::Error> {
14+
println!("Permission requested: {:?}", args);
15+
Ok(acp::RequestPermissionResponse {
16+
outcome: acp::RequestPermissionOutcome::Selected {
17+
option_id: acp::PermissionOptionId(Arc::from("allow-once")),
18+
},
19+
meta: None,
20+
})
21+
}
22+
23+
async fn write_text_file(
24+
&self,
25+
args: acp::WriteTextFileRequest,
26+
) -> Result<acp::WriteTextFileResponse, acp::Error> {
27+
println!("Write file: {:?}", args.path);
28+
Ok(acp::WriteTextFileResponse { meta: None })
29+
}
30+
31+
async fn read_text_file(
32+
&self,
33+
args: acp::ReadTextFileRequest,
34+
) -> Result<acp::ReadTextFileResponse, acp::Error> {
35+
println!("Read file: {:?}", args.path);
36+
Ok(acp::ReadTextFileResponse {
37+
content: "Hello from file!".to_string(),
38+
meta: None,
39+
})
40+
}
41+
42+
async fn create_terminal(
43+
&self,
44+
_args: acp::CreateTerminalRequest,
45+
) -> Result<acp::CreateTerminalResponse, acp::Error> {
46+
Err(acp::Error::method_not_found())
47+
}
48+
49+
async fn terminal_output(
50+
&self,
51+
_args: acp::TerminalOutputRequest,
52+
) -> Result<acp::TerminalOutputResponse, acp::Error> {
53+
Err(acp::Error::method_not_found())
54+
}
55+
56+
async fn release_terminal(
57+
&self,
58+
_args: acp::ReleaseTerminalRequest,
59+
) -> Result<acp::ReleaseTerminalResponse, acp::Error> {
60+
Err(acp::Error::method_not_found())
61+
}
62+
63+
async fn wait_for_terminal_exit(
64+
&self,
65+
_args: acp::WaitForTerminalExitRequest,
66+
) -> Result<acp::WaitForTerminalExitResponse, acp::Error> {
67+
Err(acp::Error::method_not_found())
68+
}
69+
70+
async fn kill_terminal_command(
71+
&self,
72+
_args: acp::KillTerminalCommandRequest,
73+
) -> Result<acp::KillTerminalCommandResponse, acp::Error> {
74+
Err(acp::Error::method_not_found())
75+
}
76+
77+
async fn session_notification(&self, args: acp::SessionNotification) -> Result<(), acp::Error> {
78+
match args.update {
79+
acp::SessionUpdate::AgentMessageChunk { content } => {
80+
let text = match content {
81+
acp::ContentBlock::Text(text_content) => text_content.text,
82+
_ => "<non-text>".to_string(),
83+
};
84+
println!("Agent: {}", text);
85+
}
86+
_ => {
87+
println!("Other update: {:?}", args.update);
88+
}
89+
}
90+
Ok(())
91+
}
92+
93+
async fn ext_method(
94+
&self,
95+
_method: Arc<str>,
96+
_params: Arc<RawValue>,
97+
) -> Result<Arc<RawValue>, acp::Error> {
98+
Err(acp::Error::method_not_found())
99+
}
100+
101+
async fn ext_notification(
102+
&self,
103+
_method: Arc<str>,
104+
_params: Arc<RawValue>,
105+
) -> Result<(), acp::Error> {
106+
Err(acp::Error::method_not_found())
107+
}
108+
}
109+
110+
#[tokio::main]
111+
async fn main() -> Result<()> {
112+
println!("Starting ACP client test...");
113+
114+
// Start Q CLI in ACP mode as subprocess
115+
let mut child = tokio::process::Command::new("cargo")
116+
.args(&["run", "--bin", "chat_cli", "--", "acp"])
117+
.stdin(std::process::Stdio::piped())
118+
.stdout(std::process::Stdio::piped())
119+
.stderr(std::process::Stdio::piped())
120+
.kill_on_drop(true)
121+
.spawn()?;
122+
123+
let stdin = child.stdin.take().unwrap().compat_write();
124+
let stdout = child.stdout.take().unwrap().compat();
125+
126+
let local_set = tokio::task::LocalSet::new();
127+
let result = local_set.run_until(async move {
128+
// Set up client connection
129+
let (client_conn, client_handle_io) = acp::ClientSideConnection::new(
130+
SimpleClient,
131+
stdin,
132+
stdout,
133+
|fut| { tokio::task::spawn_local(fut); }
134+
);
135+
136+
// Start I/O handler
137+
tokio::task::spawn_local(client_handle_io);
138+
139+
println!("Initializing ACP protocol...");
140+
141+
// Initialize protocol
142+
use acp::Agent;
143+
let init_response = client_conn.initialize(acp::InitializeRequest {
144+
protocol_version: acp::V1,
145+
client_capabilities: acp::ClientCapabilities::default(),
146+
meta: None,
147+
}).await?;
148+
149+
println!("Initialized! Protocol version: {:?}", init_response.protocol_version);
150+
151+
// Create session
152+
println!("Creating session...");
153+
let session_response = client_conn.new_session(acp::NewSessionRequest {
154+
mcp_servers: Vec::new(),
155+
cwd: std::env::current_dir()?,
156+
meta: None,
157+
}).await?;
158+
159+
println!("Session created: {:?}", session_response.session_id);
160+
161+
// Send a message
162+
println!("Sending message: 'Hello, Q!'");
163+
let prompt_response = client_conn.prompt(acp::PromptRequest {
164+
session_id: session_response.session_id.clone(),
165+
prompt: vec![acp::ContentBlock::Text(acp::TextContent {
166+
annotations: None,
167+
text: "Hello, Q!".to_string(),
168+
meta: None,
169+
})],
170+
meta: None,
171+
}).await?;
172+
173+
println!("Prompt response: {:?}", prompt_response.stop_reason);
174+
175+
// Wait a bit for any streaming responses
176+
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
177+
178+
println!("Test completed successfully!");
179+
180+
Ok::<(), anyhow::Error>(())
181+
}).await;
182+
183+
match result {
184+
Ok(_) => println!("ACP client test passed!"),
185+
Err(e) => println!("ACP client test failed: {}", e),
186+
}
187+
188+
Ok(())
189+
}

0 commit comments

Comments
 (0)