use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::mpsc;
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-ux30-picker-available-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
const TIMEOUT: Duration = Duration::from_secs(20);
fn run_with_timeout(home: &Path, stdin_data: &str, args: &[&str]) -> Output {
let mut cmd = Command::new(bin());
cmd.env("SUPERCODE_HOME", home)
.env("OPENROUTER_API_KEY", "sk-picker-cli-test-not-real")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("NO_COLOR")
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn the supercode binary");
let pid = child.id();
child
.stdin
.take()
.expect("stdin was piped")
.write_all(stdin_data.as_bytes())
.expect("write to child stdin");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let result = child.wait_with_output();
let _ = tx.send(result);
});
match rx.recv_timeout(TIMEOUT) {
Ok(result) => result.expect("child process failed"),
Err(mpsc::RecvTimeoutError::Timeout) | Err(mpsc::RecvTimeoutError::Disconnected) => {
let _ = Command::new("kill")
.args(["-KILL", &pid.to_string()])
.status();
panic!(
"supercode {args:?} did not exit within {TIMEOUT:?} on piped/non-tty stdin — \
the picker likely blocked reading keystrokes from a pipe nobody will ever \
write to. This is exactly the hang `picker::available()`'s TTY-gating exists \
to prevent (see picker.rs's module doc comment)."
);
}
}
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn resume_with_no_session_arg_on_piped_stdin_bails_promptly_instead_of_hanging() {
let home = fresh_home("resume-no-arg");
let out = run_with_timeout(&home, "", &["resume"]);
assert!(
!out.status.success(),
"resume with no session arg and no tty must fail (there is nothing to resume and no \
picker can run), got status={:?} stderr={}",
out.status,
stderr(&out)
);
let err = stderr(&out);
assert!(
err.contains("no interactive terminal available for the picker"),
"expected the non-tty fallback bail message, got: {err}"
);
assert!(
err.contains("pass a `.jsonl` path"),
"expected the bail message to name the workaround, got: {err}"
);
}
#[test]
fn chat_slash_model_on_piped_stdin_does_not_hang_and_prints_non_tty_fallback() {
let home = fresh_home("chat-slash-model");
let out = run_with_timeout(&home, "/model\n", &["chat"]);
assert!(
out.status.success(),
"chat fed `/model` on piped stdin should exit cleanly (EOF after the fallback line), \
got status={:?} stderr={}",
out.status,
stderr(&out)
);
let err = stderr(&out);
assert!(
err.contains("not a terminal") && err.contains("--model"),
"expected the non-tty `/model` fallback line, got: {err}"
);
}