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_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-ux22-notify-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn spawn_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);
let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"hello 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 write_fake_notify_send(dir: &Path, log_file: &Path) {
let script = format!(
"#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {log}; done\nprintf -- '---\\n' >> {log}\n",
log = log_file.display()
);
let path = dir.join("notify-send");
std::fs::write(&path, script).expect("write fake notify-send");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
}
fn run_under_pty(home: &Path, args: &[&str], path_prepend: Option<&Path>) -> (bool, String) {
let real_path = std::env::var("PATH").unwrap_or_default();
let path = match path_prepend {
Some(p) => format!("{}:{real_path}", p.display()),
None => real_path,
};
let wrapper = home.join("run.sh");
let mut body = format!("#!/bin/sh\n{:?} --bare", bin());
for a in args {
body.push_str(&format!(" {a:?}"));
}
body.push_str("\nrc=$?\nsleep 0.5\nexit $rc\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 command = Command::new("script");
#[cfg(target_os = "macos")]
command.args(["-qe", "/dev/null", &wrapper.display().to_string()]);
#[cfg(not(target_os = "macos"))]
command.args(["-qec", &wrapper.display().to_string(), "/dev/null"]);
let out = command
.env("SUPERCODE_HOME", home)
.env("PATH", path)
.env("OPENROUTER_API_KEY", "x")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("SUPERCODE_QUIET")
.env_remove("SUPERCODE_NOTIFY")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn script(1)");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(out.status.success(), combined)
}
fn run_piped(home: &Path, base_url: &str, args: &[&str], path_prepend: Option<&Path>) -> Output {
let real_path = std::env::var("PATH").unwrap_or_default();
let path = match path_prepend {
Some(p) => format!("{}:{real_path}", p.display()),
None => real_path,
};
Command::new(bin())
.args(args)
.env("SUPERCODE_HOME", home)
.env("OPENROUTER_API_KEY", "x")
.env("PATH", path)
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("SUPERCODE_QUIET")
.env_remove("SUPERCODE_NOTIFY")
.args(["--base-url", base_url])
.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 read_log(path: &Path) -> String {
std::fs::read_to_string(path).unwrap_or_default()
}
fn wait_for_complete_log_record(path: &Path, timeout: Duration) -> String {
let deadline = std::time::Instant::now() + timeout;
loop {
let contents = read_log(path);
if contents.contains("---\n") || std::time::Instant::now() >= deadline {
return contents;
}
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn fires_desktop_notification_when_enabled_over_threshold() {
let (addr, server) = spawn_sse_stub();
let home = fresh_dir("fires");
let fake_bin_dir = fresh_dir("fires-fakebin");
let log_file = fresh_dir("fires-log").join("notify.log");
write_fake_notify_send(&fake_bin_dir, &log_file);
let base_url = format!("http://{addr}");
let (ok, transcript) = run_under_pty(
&home,
&[
"--base-url",
&base_url,
"--notify",
"--notify-threshold-secs",
"0",
"run",
"say hi",
],
Some(&fake_bin_dir),
);
assert!(ok, "run under pty failed: {transcript}");
assert!(
transcript.contains("hello there"),
"expected the streamed reply in the pty transcript, got: {transcript}"
);
server.join().expect("stub server thread panicked");
let log = wait_for_complete_log_record(&log_file, Duration::from_secs(5));
assert!(
!log.is_empty(),
"fake notify-send should have been invoked; log is empty. transcript: {transcript}"
);
assert!(
log.contains("supercode: turn finished"),
"notification title missing from fake notify-send log: {log}"
);
assert!(
log.contains("hello there"),
"notification body should carry a safe preview of the reply: {log}"
);
}
#[test]
fn does_not_fire_when_notify_is_not_enabled() {
let (addr, server) = spawn_sse_stub();
let home = fresh_dir("disabled");
let fake_bin_dir = fresh_dir("disabled-fakebin");
let log_file = fresh_dir("disabled-log").join("notify.log");
write_fake_notify_send(&fake_bin_dir, &log_file);
let base_url = format!("http://{addr}");
let (ok, transcript) = run_under_pty(
&home,
&["--base-url", &base_url, "run", "say hi"],
Some(&fake_bin_dir),
);
assert!(ok, "run under pty failed: {transcript}");
server.join().expect("stub server thread panicked");
let log = read_log(&log_file);
assert!(
log.is_empty(),
"notify-send must not fire when --notify was never passed; log: {log}"
);
}
#[test]
fn degrades_silently_when_notify_send_is_absent() {
let (addr, server) = spawn_sse_stub();
let home = fresh_dir("absent");
let base_url = format!("http://{addr}");
let (ok, transcript) = run_under_pty(
&home,
&[
"--base-url",
&base_url,
"--notify",
"--notify-threshold-secs",
"0",
"run",
"say hi",
],
None, );
assert!(
ok,
"run must succeed (exit 0) even when notify-send is absent from PATH: {transcript}"
);
assert!(
transcript.contains("hello there"),
"the actual reply must still be delivered normally: {transcript}"
);
server.join().expect("stub server thread panicked");
}
#[test]
fn json_output_format_never_notifies_and_stdout_stays_clean() {
let (addr, server) = spawn_sse_stub();
let home = fresh_dir("json");
let fake_bin_dir = fresh_dir("json-fakebin");
let log_file = fresh_dir("json-log").join("notify.log");
write_fake_notify_send(&fake_bin_dir, &log_file);
let out = run_piped(
&home,
&format!("http://{addr}"),
&[
"--notify",
"--notify-threshold-secs",
"0",
"run",
"--output-format",
"json",
"say hi",
],
Some(&fake_bin_dir),
);
assert!(
out.status.success(),
"run --output-format json failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stdout_text = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(stdout_text.trim()).expect("stdout must be a single JSON envelope");
assert!(
parsed
.get("result")
.and_then(|v| v.as_str())
.unwrap_or("")
.contains("hello there"),
"stdout JSON envelope missing expected result: {stdout_text}"
);
server.join().expect("stub server thread panicked");
assert!(
read_log(&log_file).is_empty(),
"notify-send must never fire for --output-format json"
);
}