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_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-ux32-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run_non_tty(supercode_home: &Path, home: &Path, extra_args: &[&str]) -> Output {
Command::new(bin())
.env("SUPERCODE_HOME", supercode_home)
.env("HOME", home)
.env_remove("OPENROUTER_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.args(extra_args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn the supercode binary")
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn non_tty_fresh_run_never_onboards_and_keeps_the_original_no_key_error() {
let sc_home = fresh_dir("nontty-sc");
let home = fresh_dir("nontty-home");
let out = run_non_tty(&sc_home, &home, &["run", "hello"]);
assert!(
!out.status.success(),
"a genuinely key-less run must still fail: stderr={}",
stderr(&out)
);
let se = stderr(&out);
assert!(
se.contains("no API key found"),
"must keep the pre-UX-32 require_api_key message verbatim, got: {se}"
);
assert!(
!se.to_lowercase().contains("welcome to supercode"),
"a non-tty run must never show the onboarding banner: {se}"
);
assert!(
!sc_home.join("config.toml").exists(),
"a scripted/non-tty invocation must not spend the first run — \
config.toml must stay absent so a later interactive run still onboards"
);
}
#[test]
fn non_tty_json_output_with_yes_flag_still_never_onboards() {
let sc_home = fresh_dir("nontty-json-sc");
let home = fresh_dir("nontty-json-home");
let out = run_non_tty(
&sc_home,
&home,
&["--yes", "run", "--output-format", "json", "hello"],
);
assert!(!out.status.success());
let se = stderr(&out);
assert!(
se.contains("no API key found"),
"machine mode must be byte-identical to pre-UX-32 behavior: {se}"
);
assert!(
!se.to_lowercase().contains("welcome to supercode"),
"--yes must not be treated as an interactivity override: {se}"
);
}
#[test]
fn existing_config_is_never_touched_by_a_later_invocation() {
let sc_home = fresh_dir("existing-sc");
let home = fresh_dir("existing-home");
std::fs::create_dir_all(&sc_home).unwrap();
let config_text =
"model = \"anthropic/claude-opus-4-8\"\nbase_url = \"https://openrouter.ai/api/v1\"\n";
std::fs::write(sc_home.join("config.toml"), config_text).unwrap();
let out = run_non_tty(&sc_home, &home, &["run", "hello"]);
assert!(!out.status.success());
let se = stderr(&out);
assert!(
se.contains("no API key found"),
"an already-configured install with no key must still fail the same way: {se}"
);
assert!(
!se.to_lowercase().contains("welcome to supercode"),
"onboarding must never fire once config.toml exists: {se}"
);
let after = std::fs::read_to_string(sc_home.join("config.toml")).unwrap();
assert_eq!(
after, config_text,
"an existing config.toml must be byte-for-byte untouched"
);
}
fn run_under_driverless_pty(home: &Path, args: &[&str], deadline: Duration) -> (bool, String) {
run_under_driverless_pty_with_env(home, args, &[], deadline)
}
fn run_under_driverless_pty_with_env(
home: &Path,
args: &[&str],
envs: &[(&str, &str)],
deadline: Duration,
) -> (bool, String) {
let wrapper = home.join("run.sh");
let mut body = format!("#!/bin/sh\nexec {:?}", bin());
for a in args {
body.push_str(&format!(" {a:?}"));
}
body.push('\n');
std::fs::write(&wrapper, body).expect("write wrapper");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let mut cmd = Command::new("script");
#[cfg(target_os = "macos")]
cmd.args(["-qe", "/dev/null", &wrapper.display().to_string()]);
#[cfg(not(target_os = "macos"))]
cmd.args(["-qec", &wrapper.display().to_string(), "/dev/null"]);
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")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in envs {
cmd.env(k, v);
}
let mut child = cmd.spawn().expect("failed to spawn script(1)");
let deadline_at = std::time::Instant::now() + deadline;
loop {
if child
.try_wait()
.expect("try_wait should not error")
.is_some()
{
break;
}
if std::time::Instant::now() >= deadline_at {
let _ = child.kill();
let _ = child.wait();
panic!(
"supercode did not exit within {deadline:?} under a driverless pty — the \
\"onboarding never hangs when stdin can't answer\" guarantee regressed \
(see main.rs's prompt_yes_no + notify_cli.rs's module doc)"
);
}
std::thread::sleep(Duration::from_millis(20));
}
let out = child
.wait_with_output()
.expect("collect output from an already-exited child");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(out.status.success(), combined)
}
#[test]
fn first_run_under_a_driverless_pty_declines_gracefully_and_never_hangs() {
let home = fresh_dir("driverless-pty");
let (ok, transcript) = run_under_driverless_pty(
&home,
&["--base-url", "http://127.0.0.1:1", "run", "hello"],
Duration::from_secs(30),
);
assert!(
!ok,
"a genuinely key-less run must still fail even after declining onboarding: {transcript}"
);
assert!(
transcript.to_lowercase().contains("welcome to supercode"),
"the banner must still fire for a tty-shaped stdin — this proves the fix didn't just \
suppress onboarding outright, only its hang on undriven input: {transcript}"
);
assert!(
transcript.contains("skipped"),
"with no genuine input to answer 'yes' to, onboarding must degrade to the same \
decline path a human typing 'n' gets, not silently proceed: {transcript}"
);
assert!(
transcript.contains("no API key found"),
"after declining, the run must fall through to the ordinary require_api_key failure: \
{transcript}"
);
assert!(
home.join("config.toml").exists(),
"the decline marker must be written so this pty doesn't get re-asked on a future run"
);
}
#[test]
fn yes_flag_first_run_under_a_driverless_pty_completes_and_takes_the_default_model() {
let home = fresh_dir("yes-driverless-pty");
let (_ok, transcript) = run_under_driverless_pty_with_env(
&home,
&["--yes", "--base-url", "http://127.0.0.1:1", "run", "hello"],
&[("OPENROUTER_API_KEY", "x")],
Duration::from_secs(30),
);
assert!(
transcript.to_lowercase().contains("welcome to supercode"),
"the banner must still fire for a tty-shaped stdin: {transcript}"
);
assert!(
!transcript.contains("pick a default model"),
"--yes must never draw the interactive model picker at all (not even one that then \
gets auto-cancelled) — this is the picker's own header text: {transcript}"
);
assert!(
!transcript.contains("↑/↓ or j/k to move"),
"--yes must never draw the picker's key-hint chrome: {transcript}"
);
assert!(
!transcript.contains("cancelled — using the default model"),
"--yes takes the default model directly (pick_onboarding_model's own cli.yes \
short-circuit), not via a picker call that happens to get cancelled: {transcript}"
);
let config_text = std::fs::read_to_string(home.join("config.toml"))
.expect("--yes onboarding must write config.toml (the reuse branch saves it directly)");
assert!(
config_text.contains("anthropic/claude-opus-4-8"),
"--yes must land on uc::DEFAULT_MODEL when no model was explicitly chosen: {config_text}"
);
assert!(
home.join("credentials.toml").exists(),
"the reuse branch must have saved the OPENROUTER_API_KEY env key: {transcript}"
);
}