use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::time::Duration;
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-ux15-spinner-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
const RESPONSE_DELAY: Duration = Duration::from_millis(600);
fn spawn_delayed_sse_stub() -> (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 handle = std::thread::spawn(move || {
let (mut sock, _) = listener.accept().expect("accept one connection");
let mut buf = [0u8; 4096];
let _ = sock.read(&mut buf);
std::thread::sleep(RESPONSE_DELAY);
let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"hel\"}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"content\":\"lo there\"}}]}\n\n\
data: [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
);
sock.write_all(resp.as_bytes())
.expect("write stub response");
sock.flush().ok();
});
(addr, handle)
}
fn run_piped(home: &Path, base_url: &str, extra: &[&str]) -> Output {
let mut args = vec!["--api-key", "x", "--base-url", base_url];
args.extend_from_slice(extra);
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(&args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn the supercode binary")
.wait_with_output()
.expect("child process failed")
}
fn contains_spinner_bytes(raw: &[u8]) -> bool {
if raw.contains(&0x1b) {
return true; }
let text = String::from_utf8_lossy(raw);
text.contains("Thinking…")
|| text.contains("Working…")
|| ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
.iter()
.any(|f| text.contains(*f))
}
#[test]
fn piped_run_against_a_delayed_response_emits_zero_spinner_bytes() {
let (addr, server) = spawn_delayed_sse_stub();
let home = fresh_home("piped-clean");
let out = run_piped(&home, &format!("http://{addr}"), &["run", "say hi"]);
assert!(
out.status.success(),
"run failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8_lossy(&out.stdout).contains("hello there"),
"expected the streamed reply on stdout, got: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
!contains_spinner_bytes(&out.stdout),
"stdout must never carry spinner bytes; raw: {:?}",
out.stdout
);
assert!(
!contains_spinner_bytes(&out.stderr),
"piped (non-tty) stderr must carry zero spinner bytes; raw: {:?}",
out.stderr
);
server.join().expect("stub server thread panicked");
}
#[test]
fn piped_json_output_format_emits_zero_spinner_bytes() {
let (addr, server) = spawn_delayed_sse_stub();
let home = fresh_home("piped-json-clean");
let out = run_piped(
&home,
&format!("http://{addr}"),
&["run", "--output-format", "json", "say hi"],
);
assert!(
out.status.success(),
"run --output-format json failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!contains_spinner_bytes(&out.stdout),
"json stdout must never carry spinner bytes; raw: {:?}",
out.stdout
);
assert!(
!contains_spinner_bytes(&out.stderr),
"json-mode stderr must carry zero spinner bytes; raw: {:?}",
out.stderr
);
server.join().expect("stub server thread panicked");
}