use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;
fn bin() -> std::path::PathBuf {
let mut p = std::env::current_exe().expect("current exe");
p.pop(); p.pop(); p.push("trusty-memory");
p
}
fn run_piped(args: &[&str], data_dir: &std::path::Path) -> (String, String) {
let mut child = trusty_common::parent_death::exit_with_parent(
Command::new(bin())
.args(args)
.env("TRUSTY_DATA_DIR_OVERRIDE", data_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped()),
)
.spawn()
.expect("spawn trusty-memory");
drop(child.stdin.take());
let deadline = std::time::Instant::now() + Duration::from_secs(60);
loop {
if let Some(_status) = child.try_wait().expect("try_wait") {
break;
}
if std::time::Instant::now() > deadline {
let _ = child.kill();
break;
}
std::thread::sleep(Duration::from_millis(50));
}
let out = child.wait_with_output().expect("wait_with_output");
(
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
#[test]
fn bare_serve_notice_absent_when_stdin_is_piped() {
let tmp = tempfile::tempdir().expect("tempdir");
let (stdout, stderr) = run_piped(&["serve"], tmp.path());
assert!(
!stderr.contains("waiting on stdin"),
"no notice for a piped (MCP client) stdin; stderr was: {stderr}"
);
assert!(
!stdout.contains("waiting on stdin"),
"the notice must NEVER reach stdout — it is the JSON-RPC channel"
);
}
#[test]
fn bare_serve_keeps_stdout_clean() {
let tmp = tempfile::tempdir().expect("tempdir");
let (stdout, _stderr) = run_piped(&["serve"], tmp.path());
for line in stdout.lines().filter(|l| !l.trim().is_empty()) {
assert!(
serde_json::from_str::<serde_json::Value>(line).is_ok(),
"stdout must carry only JSON-RPC; found non-JSON line: {line}"
);
}
}
#[test]
fn bare_serve_and_explicit_stdio_behave_alike() {
let tmp_a = tempfile::tempdir().expect("tempdir");
let tmp_b = tempfile::tempdir().expect("tempdir");
let (out_bare, _) = run_piped(&["serve"], tmp_a.path());
let (out_flag, _) = run_piped(&["serve", "--stdio"], tmp_b.path());
let json_lines = |s: &str| {
s.lines()
.filter(|l| !l.trim().is_empty())
.filter(|l| serde_json::from_str::<serde_json::Value>(l).is_ok())
.count()
};
assert_eq!(
json_lines(&out_bare),
json_lines(&out_flag),
"bare `serve` and `serve --stdio` must produce the same stdout shape"
);
}
#[test]
fn unknown_flag_is_still_rejected() {
let out = Command::new(bin())
.args(["serve", "--definitely-not-a-flag"])
.output()
.expect("run");
assert!(!out.status.success(), "unknown flag must exit nonzero");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("unexpected argument") || stderr.contains("error"),
"expected a usage error, got: {stderr}"
);
}
#[test]
fn conflicting_transport_flags_still_rejected() {
for args in [
["serve", "--http", "--stdio"],
["serve", "--foreground", "--stdio"],
] {
let out = Command::new(bin()).args(args).output().expect("run");
assert!(
!out.status.success(),
"{args:?} must be rejected as conflicting"
);
}
}
#[test]
fn help_documents_the_new_serve_semantics() {
let out = Command::new(bin())
.args(["serve", "--help"])
.output()
.expect("run --help");
let help = String::from_utf8_lossy(&out.stdout);
assert!(
help.contains("stdio"),
"serve --help must describe the stdio default; got: {help}"
);
assert!(
help.contains("start"),
"serve --help must point at `start` for the daemon; got: {help}"
);
}
#[cfg(unix)]
#[test]
fn bare_serve_notice_present_when_stdin_is_a_tty() {
use std::os::unix::io::FromRawFd;
let tmp = tempfile::tempdir().expect("tempdir");
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
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 must succeed");
let child_stdin = unsafe { Stdio::from_raw_fd(slave) };
let mut child = trusty_common::parent_death::exit_with_parent(
Command::new(bin())
.arg("serve")
.env("TRUSTY_DATA_DIR_OVERRIDE", tmp.path())
.stdin(child_stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped()),
)
.spawn()
.expect("spawn trusty-memory on a pty");
let mut master_file = unsafe { std::fs::File::from_raw_fd(master) };
std::thread::sleep(Duration::from_millis(300));
let _ = master_file.write_all(&[0x04]);
let _ = master_file.flush();
let deadline = std::time::Instant::now() + Duration::from_secs(60);
loop {
if child.try_wait().expect("try_wait").is_some() {
break;
}
if std::time::Instant::now() > deadline {
let _ = child.kill();
break;
}
std::thread::sleep(Duration::from_millis(50));
}
let out = child.wait_with_output().expect("output");
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stderr.contains("waiting on stdin") && stderr.contains("start"),
"a human at a terminal must be told serve is stdio and start is the \
daemon verb; stderr was: {stderr}"
);
assert!(
!stdout.contains("waiting on stdin"),
"the notice must never reach stdout; stdout was: {stdout}"
);
}