use std::io::{Read, Write};
use std::net::TcpListener;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}
fn fresh_home(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-ux29-softsteer-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
struct Pty {
master: OwnedFd,
slave: RawFd,
}
impl Pty {
fn open() -> Self {
let mut master: libc::c_int = -1;
let mut slave: libc::c_int = -1;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, 0, "openpty failed: {}", std::io::Error::last_os_error());
let master = unsafe { OwnedFd::from_raw_fd(master) };
Pty { master, slave }
}
fn termios(&self) -> libc::termios {
let mut term = std::mem::MaybeUninit::<libc::termios>::uninit();
let rc = unsafe { libc::tcgetattr(self.master.as_raw_fd(), term.as_mut_ptr()) };
assert_eq!(
rc,
0,
"tcgetattr(master) failed: {}",
std::io::Error::last_os_error()
);
unsafe { term.assume_init() }
}
fn write_str(&self, s: &str) {
let fd = self.master.as_raw_fd();
let bytes = s.as_bytes();
let mut written = 0usize;
while written < bytes.len() {
let n = unsafe {
libc::write(
fd,
bytes[written..].as_ptr() as *const libc::c_void,
bytes.len() - written,
)
};
assert!(
n >= 0,
"write to pty master failed: {}",
std::io::Error::last_os_error()
);
written += n as usize;
}
}
fn expect_contains(&self, acc: &mut String, needle: &str, timeout: Duration) {
let fd = self.master.as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let deadline = Instant::now() + timeout;
let mut buf = [0u8; 4096];
loop {
if acc.contains(needle) {
return;
}
if Instant::now() > deadline {
panic!("timed out waiting for {needle:?} in pty output; captured so far: {acc:?}");
}
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n > 0 {
acc.push_str(&String::from_utf8_lossy(&buf[..n as usize]));
} else {
std::thread::sleep(Duration::from_millis(10));
}
}
}
fn close_slave(&self) {
unsafe {
libc::close(self.slave);
}
}
}
fn spawn_chat(pty: &Pty, home: &Path, base_url: &str) -> std::process::Child {
let dup_for = |slave: RawFd| -> Stdio {
let duped = unsafe { libc::dup(slave) };
assert!(
duped >= 0,
"dup(slave) failed: {}",
std::io::Error::last_os_error()
);
let owned = unsafe { OwnedFd::from_raw_fd(duped) };
Stdio::from(owned)
};
let slave = pty.slave;
let mut cmd = Command::new(bin());
cmd.env("SUPERCODE_HOME", home)
.env("HOME", home)
.env("XDG_CONFIG_HOME", home.join("config"))
.env("XDG_DATA_HOME", home.join("data"))
.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, "chat"])
.stdin(dup_for(slave))
.stdout(dup_for(slave))
.stderr(dup_for(slave));
unsafe {
cmd.pre_exec(move || {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
if libc::ioctl(slave, libc::TIOCSCTTY as libc::c_ulong, 0) == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
cmd.spawn()
.expect("failed to spawn supercode chat over a pty")
}
fn spawn_multi_stub(
delays: Vec<Duration>,
reply: &'static str,
) -> (
std::net::SocketAddr,
std::thread::JoinHandle<()>,
mpsc::Receiver<Vec<u8>>,
) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub listener");
let addr = listener.local_addr().unwrap();
let (tx, rx) = mpsc::channel();
let handle = std::thread::spawn(move || {
for i in 0.. {
let Ok((mut sock, _)) = listener.accept() else {
break;
};
let mut buf = vec![0u8; 16384];
let n = sock.read(&mut buf).unwrap_or(0);
if n > 0 && tx.send(buf[..n].to_vec()).is_err() {
break; }
let delay = *delays.get(i).unwrap_or_else(|| delays.last().unwrap());
std::thread::sleep(delay);
let sse = format!(
"data: {{\"choices\":[{{\"delta\":{{\"content\":\"{reply}\"}}}}]}}\n\ndata: [DONE]\n\n"
);
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
);
let _ = sock.write_all(resp.as_bytes());
let _ = sock.flush();
}
});
(addr, handle, rx)
}
#[test]
#[cfg_attr(
target_os = "macos",
ignore = "macOS openpty does not deliver synthetic NL/CR as a rustyline Enter key"
)]
fn steer_mid_turn_is_queued_and_delivered_without_aborting_the_turn_then_ctrl_c_still_hard_aborts()
{
let home = fresh_home("main");
std::fs::write(
home.join("config.toml"),
"model = \"anthropic/claude-opus-4-8\"\n",
)
.expect("seed config.toml");
let pty = Pty::open();
let orig_termios = pty.termios();
let (addr, server, rx_bodies) = spawn_multi_stub(
vec![Duration::from_millis(1000), Duration::from_secs(4)],
"hello from turn one",
);
let mut child = spawn_chat(&pty, &home, &format!("http://{addr}"));
let mut out = String::new();
pty.expect_contains(&mut out, "Ctrl-D to exit", Duration::from_secs(10));
pty.write_str("hello there\n");
let req0 = rx_bodies
.recv_timeout(Duration::from_secs(10))
.expect("stub never saw turn 1's request arrive — turn never left the client");
assert!(
String::from_utf8_lossy(&req0).contains("hello there"),
"turn 1's request should carry the typed message"
);
pty.write_str("focus on unit tests next\n");
pty.expect_contains(
&mut out,
"queued — will steer after this turn",
Duration::from_secs(5),
);
assert!(
!out.contains("hello from turn one"),
"the steer notice must land BEFORE turn 1's reply — got: {out:?}"
);
pty.expect_contains(&mut out, "hello from turn one", Duration::from_secs(10));
let req1 = rx_bodies
.recv_timeout(Duration::from_secs(10))
.expect("the queued steer was never delivered as a second request");
let req1_str = String::from_utf8_lossy(&req1);
assert!(
req1_str.contains("focus on unit tests next"),
"the second request must carry the exact queued steer text, got: {req1_str}"
);
assert!(
req1_str.contains("hello there") && req1_str.contains("hello from turn one"),
"the second request's history must still include turn 1's own exchange \
(the steer is delivered as a NEW turn, not a replacement), got: {req1_str}"
);
pty.write_str("\u{3}"); pty.expect_contains(&mut out, "interrupted", Duration::from_secs(5));
drop(server);
assert_eq!(
out.matches("hello from turn one").count(),
1,
"the delivered steer's own turn must never complete after a hard Ctrl-C \
(its reply text must never appear a second time), got: {out:?}"
);
assert_eq!(
out.matches("queued — will steer").count(),
1,
"exactly one steer should ever have been queued (from turn 1) — a Ctrl-C must never \
itself be captured as a second steer line: {out:?}"
);
pty.write_str("\u{4}"); let status = child.wait().expect("child wait failed");
assert!(
status.success(),
"the REPL must still be alive and exit cleanly via Ctrl-D after the hard-cancel: \
status={status:?} pty output so far={out:?}"
);
let after_termios = pty.termios();
assert_eq!(
(
orig_termios.c_iflag,
orig_termios.c_oflag,
orig_termios.c_cflag,
orig_termios.c_lflag,
),
(
after_termios.c_iflag,
after_termios.c_oflag,
after_termios.c_cflag,
after_termios.c_lflag,
),
"the pty's termios must be restored to their pre-session state after the child exits"
);
pty.close_slave();
}
#[test]
fn non_tty_run_never_emits_soft_steer_chrome() {
let home = fresh_home("nontty");
let (addr, server, _rx) = spawn_multi_stub(vec![Duration::from_millis(0)], "hi");
let out = Command::new(bin())
.env("SUPERCODE_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",
&format!("http://{addr}"),
"run",
"a piped one-shot prompt",
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to run supercode");
drop(server); assert!(
out.status.success(),
"piped run should succeed: status={:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stderr.contains("queued — will steer") && !stdout.contains("queued — will steer"),
"a piped/non-tty run must never emit soft-steer chrome, stderr={stderr} stdout={stdout}"
);
}