use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::Duration;
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}
fn fresh_dir(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"supercode-servergate-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn base_cmd(home: &Path, project_dir: &Path, base_url: &str) -> Command {
let mut cmd = Command::new(bin());
cmd.current_dir(project_dir)
.env("SUPERCODE_HOME", home)
.env("HOME", home)
.env_remove("OPENROUTER_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("NO_COLOR")
.env_remove("SUPERCODE_QUIET")
.args(["--api-key", "x", "--base-url", base_url]);
cmd
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
const HOSTILE_PROJECT_TOML: &str =
"schema_version = 1\n[capabilities.server]\nenabled = true\nbind = \"0.0.0.0:9\"\n";
#[test]
fn hostile_project_config_cannot_make_serve_open_a_listener() {
let home = fresh_dir("serve-off-home");
let project = fresh_dir("serve-off-project");
std::fs::write(project.join(".supercode.toml"), HOSTILE_PROJECT_TOML).unwrap();
let out = base_cmd(&home, &project, "http://127.0.0.1:1")
.args(["serve", "--no-tmux"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn the supercode binary");
assert!(
!out.status.success(),
"`serve` must refuse when only a PROJECT config enabled [capabilities.server]"
);
assert_eq!(
stdout(&out),
"",
"no stdout at all — the refusal must happen before any listener/server output"
);
let err = stderr(&out);
assert!(
err.contains("capabilities.server"),
"expected the project-sanitization warning to name capabilities.server, got: {err}"
);
assert!(
err.contains("[capabilities.server] enabled = true"),
"expected the refusal error to explain how to actually enable it, got: {err}"
);
assert!(
!err.contains("listening on http://"),
"must never reach the point of binding/reporting a listener, got: {err}"
);
}
#[test]
fn hostile_project_config_cannot_make_run_rpc_mode_start_either() {
let home = fresh_dir("rpc-off-home");
let project = fresh_dir("rpc-off-project");
std::fs::write(project.join(".supercode.toml"), HOSTILE_PROJECT_TOML).unwrap();
let out = base_cmd(&home, &project, "http://127.0.0.1:1")
.args(["run", "--output-format", "rpc"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn the supercode binary");
assert!(
!out.status.success(),
"`run --output-format rpc` must refuse when only a PROJECT config enabled the capability"
);
assert_eq!(stdout(&out), "");
assert!(stderr(&out).contains("capabilities.server"));
}
#[test]
fn server_capability_disabled_entirely_refuses_serve_with_no_project_config_at_all() {
let home = fresh_dir("serve-default-home");
let project = fresh_dir("serve-default-project");
let out = base_cmd(&home, &project, "http://127.0.0.1:1")
.args(["serve", "--no-tmux"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn the supercode binary");
assert!(!out.status.success());
assert_eq!(stdout(&out), "");
assert!(stderr(&out).contains("[capabilities.server] enabled = true"));
}
fn spawn_text_stub(text: &str) -> (std::net::SocketAddr, std::thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub listener");
let addr = listener.local_addr().unwrap();
let text = text.to_string();
let handle = std::thread::spawn(move || {
let (mut sock, _) = listener.accept().expect("accept one connection");
sock.set_read_timeout(Some(Duration::from_millis(500))).ok();
let mut buf = Vec::new();
let mut chunk = [0u8; 65536];
loop {
match sock.read(&mut chunk) {
Ok(0) => break,
Ok(n) => buf.extend_from_slice(&chunk[..n]),
Err(_) => break,
}
}
let sse = format!(
"data: {{\"choices\":[{{\"delta\":{{\"content\":{}}}}}]}}\n\ndata: [DONE]\n\n",
serde_json::to_string(&text).unwrap()
);
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
sse.len(),
sse
);
sock.write_all(resp.as_bytes()).ok();
sock.flush().ok();
});
(addr, handle)
}
struct ChildGuard(Child);
impl Drop for ChildGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[tokio::test]
async fn trusted_user_config_enables_serve_and_a_real_rpc_round_trip_works() {
let home = fresh_dir("serve-on-home");
let project = fresh_dir("serve-on-project");
std::fs::write(
home.join("config.toml"),
"schema_version = 1\n[capabilities.server]\nenabled = true\n",
)
.unwrap();
let (addr, _stub) = spawn_text_stub("hello from the real server");
let token = "server-capability-private-bearer";
let mut child = ChildGuard(
base_cmd(&home, &project, &format!("http://{addr}"))
.env("SUPERCODE_SERVER_TOKEN", token)
.args(["serve", "--no-tmux", "--bind", "127.0.0.1:0"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn the supercode binary"),
);
let stderr = child.0.stderr.take().unwrap();
let mut reader = std::io::BufReader::new(stderr);
let mut captured_stderr = String::new();
let base_url = read_listen_line(&mut reader, &mut captured_stderr);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.post(format!("{base_url}/rpc"))
.bearer_auth(token)
.json(&serde_json::json!({"id": 1, "method": "submit", "params": {"prompt": "hi"}}))
.send()
.await
.expect("POST /rpc submit should reach the real server");
assert_eq!(resp.status().as_u16(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["result"]["reply"], "hello from the real server");
let unauthed = client
.post(format!("{base_url}/rpc"))
.json(&serde_json::json!({"id": 2, "method": "status"}))
.send()
.await
.unwrap();
assert_eq!(unauthed.status().as_u16(), 401);
let _ = client
.post(format!("{base_url}/rpc"))
.bearer_auth(token)
.json(&serde_json::json!({"id": 3, "method": "shutdown"}))
.send()
.await;
let status = child
.0
.wait_timeout_or_kill(Duration::from_secs(5))
.expect("process should exit after `shutdown`");
assert!(status.success(), "expected a clean exit after shutdown");
reader.read_to_string(&mut captured_stderr).unwrap();
let mut captured_stdout = String::new();
child
.0
.stdout
.take()
.unwrap()
.read_to_string(&mut captured_stdout)
.unwrap();
assert!(
!captured_stderr.contains(token) && !captured_stdout.contains(token),
"server output leaked its bearer: stdout={captured_stdout:?} stderr={captured_stderr:?}"
);
}
fn read_listen_line(reader: &mut impl std::io::BufRead, captured: &mut String) -> String {
let mut base_url = None;
let mut line = String::new();
for _ in 0..50 {
line.clear();
let n = reader.read_line(&mut line).unwrap_or(0);
if n == 0 {
break;
}
captured.push_str(&line);
if let Some(rest) = line.trim().strip_prefix("supercode server listening on ") {
let url = rest.split_whitespace().next().unwrap_or("").to_string();
base_url = Some(url);
}
if base_url.is_some() {
break;
}
}
base_url.expect("expected a `listening on http://...` line on stderr")
}
trait WaitTimeoutOrKill {
fn wait_timeout_or_kill(
&mut self,
timeout: Duration,
) -> std::io::Result<std::process::ExitStatus>;
}
impl WaitTimeoutOrKill for Child {
fn wait_timeout_or_kill(
&mut self,
timeout: Duration,
) -> std::io::Result<std::process::ExitStatus> {
let start = std::time::Instant::now();
loop {
if let Some(status) = self.try_wait()? {
return Ok(status);
}
if start.elapsed() > timeout {
let _ = self.kill();
return self.wait();
}
std::thread::sleep(Duration::from_millis(20));
}
}
}