supercode-cli 0.4.8

supercode — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
//! Process-level proof that the public `supercode acp` surface is a standard
//! ACP transport over the SDK-owned runtime, including persistence.

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();
}