use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use supercode::server::{run_http, RpcEngine};
use supercode::{Agent, ChatMessage, ChatRequest, Config, Provider, Usage};
use tokio::io::AsyncWriteExt;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_supercode")
}
struct UnusedProvider;
#[async_trait]
impl Provider for UnusedProvider {
async fn complete(
&self,
_request: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode::Result<(ChatMessage, Usage)> {
panic!("detaching an ACP frontend must not call the model")
}
}
#[tokio::test]
async fn public_attach_uses_acp_bridge_without_stopping_the_runtime() -> Result<()> {
let cwd = std::env::temp_dir().join(format!("supercode-acp-attach-cli-{}", std::process::id()));
std::fs::create_dir_all(&cwd)?;
let agent = Agent::with_provider(
Config::builder().cwd(cwd.clone()).build(),
Box::new(UnusedProvider),
);
let runtime = RpcEngine::new_named(agent, "public-acp-attach", None);
let token: Arc<str> = "acp-attach-test-token".into();
let address = run_http(runtime.clone(), "127.0.0.1:0", token.clone()).await?;
let url = format!("http://{address}");
let supercode_home = cwd.join("supercode-home");
for invocation in 1..=2 {
let mut child = tokio::process::Command::new(bin())
.current_dir(&cwd)
.env("SUPERCODE_HOME", &supercode_home)
.env("SUPERCODE_SERVER_TOKEN", token.as_ref())
.arg("--bare")
.arg("attach")
.arg("--connect")
.arg(&url)
.arg("--acp")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = child.stdin.take().expect("attach stdin must be piped");
stdin.write_all(b"/detach\n").await?;
drop(stdin);
let output = tokio::time::timeout(Duration::from_secs(30), child.wait_with_output())
.await
.expect("ACP attach command timed out")?;
assert!(
output.status.success(),
"attach invocation {invocation} failed: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("attached to public-acp-attach"), "{stdout}");
assert!(
stdout.contains("system>"),
"bounded history was not rendered: {stdout}"
);
assert!(
!runtime.is_shutting_down(),
"ACP detach invocation {invocation} stopped its SDK-owned runtime"
);
}
let checkpoint_dir = supercode_home.join("frontend-acp");
let checkpoints = std::fs::read_dir(&checkpoint_dir)?.collect::<Result<Vec<_>, _>>()?;
assert_eq!(checkpoints.len(), 1);
let checkpoint: supercode::AcpFrontendCheckpoint =
serde_json::from_slice(&std::fs::read(checkpoints[0].path())?)?;
assert_eq!(checkpoint.session_id, "public-acp-attach");
runtime.shutdown().await;
std::fs::remove_dir_all(cwd).ok();
Ok(())
}