use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_supercode")
}
fn temp_home() -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let home =
std::env::temp_dir().join(format!("supercode-acp-cli-{}-{nonce}", std::process::id()));
fs::create_dir_all(&home).unwrap();
home
}
fn spawn_sse_stub() -> (
std::net::SocketAddr,
mpsc::Receiver<String>,
std::thread::JoinHandle<()>,
) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = mpsc::channel();
let handle = std::thread::spawn(move || {
let (mut socket, _) = listener.accept().unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let mut request = Vec::new();
let mut buf = [0_u8; 4096];
loop {
let read = socket.read(&mut buf).unwrap();
if read == 0 {
break;
}
request.extend_from_slice(&buf[..read]);
let Some(header_end) = request.windows(4).position(|w| w == b"\r\n\r\n") else {
continue;
};
let headers = String::from_utf8_lossy(&request[..header_end]);
let content_len = headers
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse::<usize>().ok())
.flatten()
})
.unwrap_or(0);
if request.len() >= header_end + 4 + content_len {
break;
}
}
tx.send(String::from_utf8_lossy(&request).into_owned())
.unwrap();
let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"ACP CLI DONE\"}}]}\n\n\
data: [DONE]\n\n";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{sse}",
sse.len()
);
socket.write_all(response.as_bytes()).unwrap();
});
(addr, rx, handle)
}
fn send(stdin: &mut impl Write, value: serde_json::Value) {
writeln!(stdin, "{value}").unwrap();
stdin.flush().unwrap();
}
fn receive(stdout: &mut impl BufRead) -> serde_json::Value {
let mut line = String::new();
stdout.read_line(&mut line).unwrap();
assert!(!line.is_empty(), "ACP process closed unexpectedly");
serde_json::from_str(&line).unwrap()
}
#[test]
fn public_acp_command_negotiates_streams_and_persists_through_the_sdk_runtime() {
let home = temp_home();
let (addr, request_rx, server) = spawn_sse_stub();
let mut child = Command::new(bin())
.env("HOME", &home)
.env("SUPERCODE_HOME", home.join("supercode-home"))
.env_remove("OPENROUTER_API_KEY")
.args([
"--bare",
"--quiet",
"--api-key",
"x",
"--base-url",
&format!("http://{addr}"),
"--max-iterations",
"1",
"acp",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let mut stdout = BufReader::new(child.stdout.take().unwrap());
send(
&mut stdin,
serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": 1, "clientCapabilities": {}}
}),
);
assert_eq!(receive(&mut stdout)["result"]["protocolVersion"], 1);
send(
&mut stdin,
serde_json::json!({
"jsonrpc": "2.0", "id": 2, "method": "session/new",
"params": {"cwd": home, "mcpServers": []}
}),
);
let opened = receive(&mut stdout);
let session_id = opened["result"]["sessionId"].as_str().unwrap().to_string();
assert!(!session_id.is_empty());
send(
&mut stdin,
serde_json::json!({
"jsonrpc": "2.0", "id": 3, "method": "session/prompt",
"params": {
"sessionId": session_id,
"prompt": [{"type": "text", "text": "ACP_INPUT_MARKER"}]
}
}),
);
let mut messages = Vec::new();
loop {
let message = receive(&mut stdout);
let terminal = message.get("id").and_then(serde_json::Value::as_u64) == Some(3);
messages.push(message);
if terminal {
break;
}
}
assert!(messages.iter().any(|message| {
message
.pointer("/params/update/content/text")
.and_then(serde_json::Value::as_str)
== Some("ACP CLI DONE")
}));
assert!(messages.iter().any(|message| {
message.get("id").and_then(serde_json::Value::as_u64) == Some(3)
&& message
.pointer("/result/stopReason")
.and_then(serde_json::Value::as_str)
== Some("end_turn")
}));
let request = request_rx
.recv_timeout(Duration::from_secs(10))
.expect("ACP prompt should reach the provider through the SDK runtime");
assert!(request.contains("ACP_INPUT_MARKER"), "{request}");
drop(stdin);
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let sessions = home.join("supercode-home").join("sessions");
assert!(fs::read_dir(sessions).unwrap().any(|entry| {
let path = entry.unwrap().path();
path.extension().and_then(|extension| extension.to_str()) == Some("jsonl")
&& fs::read_to_string(path)
.unwrap_or_default()
.contains("ACP CLI DONE")
}));
server.join().unwrap();
fs::remove_dir_all(home).ok();
}