use std::process::Command;
use marver::daemon::Config;
use tempfile::TempDir;
fn run(args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_marver"))
.args(args)
.output()
.expect("run marver")
}
#[test]
fn the_version_flag_reports_the_crate_version() {
for flag in ["-v", "-V", "--version"] {
let out = run(&[flag]);
let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert!(out.status.success(), "{flag} should exit 0");
assert_eq!(text, format!("marver {}", env!("CARGO_PKG_VERSION")));
}
}
#[test]
fn the_version_flag_does_not_open_the_interface() {
let out = run(&["--version"]);
let text = String::from_utf8_lossy(&out.stdout);
assert!(!text.contains("Tasks"), "{text:?}");
assert!(text.lines().count() == 1, "one line and no more: {text:?}");
}
#[test]
fn an_unknown_subcommand_is_refused_rather_than_assumed() {
let out = run(&["wat"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(stderr.contains("unknown command"), "{stderr:?}");
assert!(
stderr.contains("usage:"),
"and it shows what to do: {stderr:?}"
);
}
#[test]
fn an_unknown_option_is_refused_instead_of_opening_the_interface() {
let dir = TempDir::new().unwrap();
for bad in ["--verbose", "-x", "--dat-dir"] {
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args([bad, "--data-dir"])
.arg(dir.path())
.output()
.expect("run marver");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{bad} should be refused");
assert!(stderr.contains("unknown option"), "{bad}: {stderr:?}");
assert!(stderr.contains("usage:"), "{bad}: {stderr:?}");
}
assert!(
!marver::daemon::is_running(&Config::new(dir.path(), dir.path())),
"a refused option must not have started anything"
);
}
#[test]
fn known_options_still_reach_the_interface() {
let dir = TempDir::new().unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["--data-dir"])
.arg(dir.path())
.output()
.expect("run marver");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("unknown option"),
"a real option was rejected: {stderr:?}"
);
let config = Config::new(dir.path(), dir.path());
if marver::daemon::is_running(&config) {
let _ = Command::new("pkill")
.arg("-f")
.arg(format!("marver daemon --data-dir {}", dir.path().display()))
.status();
}
}
fn stub_cargo(dir: &TempDir, exit: i32) -> std::path::PathBuf {
let path = dir.path().join("stub-cargo");
std::fs::write(
&path,
format!(
"#!/bin/sh\necho \"$@\" > {}/cargo-args\nexit {exit}\n",
dir.path().display()
),
)
.expect("write stub");
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("chmod");
path
}
#[test]
fn upgrade_asks_cargo_for_the_published_crate() {
let dir = TempDir::new().unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["upgrade", "--data-dir"])
.arg(dir.path())
.env("MARVER_CARGO", stub_cargo(&dir, 0))
.output()
.expect("run marver upgrade");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "{stderr:?}");
let asked = std::fs::read_to_string(dir.path().join("cargo-args")).expect("stub ran");
assert_eq!(asked.trim(), "install marver --force");
assert!(stderr.contains(env!("CARGO_PKG_VERSION")), "{stderr:?}");
}
#[test]
fn a_failed_upgrade_says_nothing_changed() {
let dir = TempDir::new().unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["upgrade", "--data-dir"])
.arg(dir.path())
.env("MARVER_CARGO", stub_cargo(&dir, 1))
.output()
.expect("run marver upgrade");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a failed install must not report success"
);
assert!(stderr.contains("nothing was changed"), "{stderr:?}");
}
#[test]
fn upgrade_without_cargo_explains_rather_than_failing_obscurely() {
let dir = TempDir::new().unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["upgrade", "--data-dir"])
.arg(dir.path())
.env("MARVER_CARGO", dir.path().join("no-such-cargo"))
.output()
.expect("run marver upgrade");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(
stderr.contains("the way you installed it"),
"it should point elsewhere, not just error: {stderr:?}"
);
}
#[test]
fn output_survives_a_reader_that_stops_reading() {
let dir = TempDir::new().unwrap();
let piped = Command::new("sh")
.arg("-c")
.arg(format!(
"{} scan {} | head -1",
env!("CARGO_BIN_EXE_marver"),
dir.path().display()
))
.output()
.expect("run the pipeline");
let stderr = String::from_utf8_lossy(&piped.stderr);
assert!(
!stderr.contains("panicked"),
"a closed pipe must not panic: {stderr:?}"
);
assert!(!stderr.contains("Broken pipe"), "{stderr:?}");
}
#[test]
fn help_asked_for_goes_to_stdout_and_succeeds() {
let out = run(&["--help"]);
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "help was asked for, not misused");
assert!(
String::from_utf8_lossy(&out.stderr).is_empty(),
"nothing went wrong, so stderr stays quiet"
);
for command in ["status", "daemon", "scan", "hook", "--version"] {
assert!(
text.contains(command),
"{command} missing from help: {text:?}"
);
}
}
#[test]
fn help_as_a_correction_goes_to_stderr_and_fails() {
let out = run(&["wat"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(stderr.contains("usage:"), "{stderr:?}");
assert!(
String::from_utf8_lossy(&out.stdout).is_empty(),
"a correction does not belong on stdout"
);
}
#[test]
fn a_codex_notify_payload_is_read_from_its_argument_rather_than_stdin() {
let tmp = TempDir::new().unwrap();
let socket = tmp.path().join("s.sock");
let listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
let heard = std::thread::spawn(move || {
use std::io::Read;
let (mut stream, _) = listener.accept().unwrap();
let mut body = String::new();
stream.read_to_string(&mut body).unwrap();
body
});
let payload = r#"{"type":"agent-turn-complete","turn-id":"7","cwd":"/tmp"}"#;
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["hook", "--task", "3", "--socket"])
.arg(&socket)
.args(["--from", "codex", payload])
.stdin(std::process::Stdio::null())
.output()
.expect("run marver hook");
assert!(out.status.success(), "a hook always exits 0: {out:?}");
let body = heard.join().unwrap();
assert!(body.contains("agent-turn-complete"), "{body}");
assert!(body.contains("\"task_id\":3"), "{body}");
}
#[test]
fn an_unknown_harness_is_refused_before_anything_is_started() {
let out = run(&["--harness", "nosuchthing"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(stderr.contains("no harness called"), "{stderr:?}");
assert!(stderr.contains("claude, codex"), "{stderr:?}");
}