use std::process::Command;
use super::common::{assert_contains, init_test_logging};
#[cfg(unix)]
#[test]
fn shim_status_reports_actual_path_interception_and_absolute_toolchain_gap() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
let home = temp.path();
let shim_dir = home.join(".rch/shims");
let delegator_dir = home.join(".local/bin");
let real_dir = home.join(".cargo/bin");
let nonexec_dir = home.join("nonexec");
let toolchains = home.join(".rustup/toolchains");
let toolchain_bin = toolchains.join("fixture-toolchain/bin");
let config_dir = home.join("config");
for dir in [
&shim_dir,
&delegator_dir,
&real_dir,
&nonexec_dir,
&toolchain_bin,
&config_dir,
] {
std::fs::create_dir_all(dir).unwrap();
}
let executable = |path: &std::path::Path, body: &str| {
std::fs::write(path, body).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
};
executable(
&shim_dir.join("cargo"),
"#!/bin/sh\n# rch-shim-version: 4\nprintf 'intercepted\\n'\n",
);
executable(
&delegator_dir.join("cargo"),
"#!/bin/sh\nSHIM=\"$HOME/.rch/shims/cargo\"\nif [ -x \"$SHIM\" ]; then exec \"$SHIM\" \"$@\"; fi\nexec \"$HOME/.cargo/bin/cargo\" \"$@\"\n",
);
for dir in [real_dir.as_path(), &toolchain_bin, home] {
executable(&dir.join("cargo"), "#!/bin/sh\nprintf 'real\\n'\n");
}
let nonexec = nonexec_dir.join("cargo");
std::fs::write(&nonexec, "not an executable cargo").unwrap();
std::fs::set_permissions(&nonexec, std::fs::Permissions::from_mode(0o644)).unwrap();
std::fs::write(
config_dir.join("config.toml"),
"[general]\nenabled = true\n",
)
.unwrap();
std::fs::write(config_dir.join("workers.toml"), "workers = []\n").unwrap();
for (first, expected, shell_output, human_message) in [
(
shim_dir.as_path(),
"direct",
"intercepted\n",
"PATH resolves the shim ahead",
),
(
&nonexec_dir,
"direct",
"intercepted\n",
"PATH resolves the shim ahead",
),
(
&delegator_dir,
"delegated",
"intercepted\n",
"cargo IS intercepted",
),
(
&real_dir,
"none",
"real\n",
"cargo is not being intercepted",
),
(
std::path::Path::new(""),
"none",
"real\n",
"cargo is not being intercepted",
),
] {
let path = std::env::join_paths([first, &shim_dir, &real_dir]).unwrap();
let actual = Command::new("/bin/sh")
.args(["-c", "cargo"])
.env_clear()
.env("HOME", home)
.env("PATH", &path)
.current_dir(home)
.output()
.unwrap();
assert!(actual.status.success(), "{expected}: {actual:?}");
assert_eq!(actual.stdout, shell_output.as_bytes(), "{expected}");
for machine in [true, false] {
let mut command = Command::new(env!("CARGO_BIN_EXE_rch"));
command
.env_clear()
.env("HOME", home)
.env("PATH", &path)
.env("RUSTUP_HOME", home.join(".rustup"))
.env("RCH_CONFIG_DIR", &config_dir)
.env("XDG_CACHE_HOME", home.join("cache"))
.env("NO_COLOR", "1")
.current_dir(home)
.arg("--no-self-healing");
if machine {
command.args(["--json", "--format=json"]);
}
let output = command.args(["shim", "status"]).output().unwrap();
assert!(output.status.success(), "{expected}: {output:?}");
if machine {
let status: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(status["installed"], true, "{status}");
assert_eq!(status["interception"], expected, "{status}");
assert_eq!(status["on_path_ahead_of_cargo"], expected != "none");
assert_eq!(status["toolchains_wrapped"], 0, "{status}");
assert_eq!(status["toolchains_total"], 1, "{status}");
} else {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains(human_message), "{expected}: {stdout}");
assert!(
stdout.contains(
"1 unwrapped toolchain(s) can still build locally via absolute path"
),
"{expected}: {stdout}"
);
assert!(stdout.contains("rch shim install"), "{stdout}");
if expected != "none" {
assert!(
!stdout.contains("cargo is not being intercepted"),
"{stdout}"
);
}
}
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn dispatcher_local_build_warning_reaches_status_doctor_and_watch_without_daemon() {
use std::io::{BufReader, Read, Write};
use std::process::{Child, Stdio};
use std::time::{Duration, Instant};
struct HeldChild(Child);
impl Drop for HeldChild {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
let temp = tempfile::tempdir().unwrap();
let config_dir = temp.path().join("config");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(config_dir.join("config.toml"),
"[general]\nrole = \"dispatcher\"\n[self_healing]\nhook_starts_daemon = false\ndaemon_installs_hooks = false\n").unwrap();
std::fs::write(config_dir.join("workers.toml"), "workers = []\n").unwrap();
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let spawn_cargo = |managed: bool| {
let cargo_home = temp.path().join(if managed {
"cargo-managed"
} else {
"cargo-unmanaged"
});
std::fs::create_dir_all(&cargo_home).unwrap();
let mut command = Command::new(&cargo);
command
.arg("login")
.env("CARGO_HOME", cargo_home)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::inherit());
if managed {
command.env("RCH_CARGO_WRAPPER_BYPASS", "1");
} else {
command.env_remove("RCH_CARGO_WRAPPER_BYPASS");
}
let mut child = HeldChild(command.spawn().expect("spawn real cargo login"));
let deadline = Instant::now() + Duration::from_secs(10);
loop {
assert!(
child.0.try_wait().unwrap().is_none(),
"cargo login exited before observation"
);
let comm =
std::fs::read_to_string(format!("/proc/{}/comm", child.0.id())).unwrap_or_default();
if matches!(comm.trim(), "cargo" | "cargo-rch-real") {
break;
}
assert!(
Instant::now() < deadline,
"cargo never became observable: {comm:?}"
);
std::thread::sleep(Duration::from_millis(25));
}
child
};
let unmanaged = spawn_cargo(false);
let managed = spawn_cargo(true);
let unmanaged_pid = format!("pid={} ", unmanaged.0.id());
let managed_pid = format!("pid={} ", managed.0.id());
let cli = |machine: bool| {
let mut command = Command::new(env!("CARGO_BIN_EXE_rch"));
command
.current_dir(temp.path())
.env("HOME", temp.path())
.env("XDG_CACHE_HOME", temp.path().join("cache"))
.env("XDG_CONFIG_HOME", temp.path().join("xdg-config"))
.env("RCH_CONFIG_DIR", &config_dir)
.env("RCH_SOCKET_PATH", temp.path().join("absent.sock"))
.env("RCH_LOG_LEVEL", "warn")
.env_remove("RUST_LOG")
.env_remove("RCH_LOG_FILE")
.env_remove("RCH_DAEMON_SOCKET")
.env_remove("RCH_JSON")
.env_remove("RCH_OUTPUT_FORMAT")
.env_remove("TOON_DEFAULT_FORMAT")
.arg("--no-self-healing");
if machine {
command.args(["--json", "--format=json"]);
}
command
};
let assert_attribution = |detail: &str| {
assert!(
detail.contains(&unmanaged_pid),
"missing unmanaged PID: {detail}"
);
assert!(
!detail.contains(&managed_pid),
"managed PID was reported: {detail}"
);
};
let human = cli(false).arg("status").output().unwrap();
let stderr = String::from_utf8_lossy(&human.stderr);
assert_attribution(&stderr);
assert_eq!(
stderr.matches("RCH LOCAL BUILD ALARM:").count(),
1,
"{stderr}"
);
for args in [
vec!["status"],
vec!["status", "--fleet"],
vec!["status", "--remediation"],
] {
let output = cli(true).args(&args).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("RCH LOCAL BUILD ALARM:"), "{stderr}");
assert!(
!output.status.success(),
"missing daemon must retain failure status"
);
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let warning = value["error"]["context"]["local_build_warning"]
.as_str()
.unwrap_or_else(|| {
panic!(
"{args:?} omitted local_build_warning: exit={} JSON={value} stderr={stderr}",
output.status
)
});
assert_attribution(warning);
}
for (index, body) in ["not-json", "{\"daemon\":"].into_iter().enumerate() {
let socket = temp.path().join(format!("invalid-{index}.sock"));
let listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0; 12];
stream.read_exact(&mut request).unwrap();
assert_eq!(&request, b"GET /status\n");
write!(stream, "HTTP/1.1 200 OK\r\n\r\n{body}").unwrap();
});
let output = cli(true)
.env("RCH_SOCKET_PATH", socket)
.arg("status")
.output()
.unwrap();
server.join().unwrap();
assert!(!output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_attribution(
value["error"]["context"]["local_build_warning"]
.as_str()
.unwrap_or_else(|| {
panic!(
"malformed daemon body {body:?} omitted local_build_warning: JSON={value} stderr={}",
String::from_utf8_lossy(&output.stderr)
)
}),
);
}
for args in [
vec!["doctor"],
vec!["doctor", "--reliability", "--scope=triage"],
] {
let output = cli(true).args(&args).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("RCH LOCAL BUILD ALARM:"), "{stderr}");
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let collection = if args.len() == 1 {
"checks"
} else {
"diagnostics"
};
let check = value["data"][collection]
.as_array()
.unwrap()
.iter()
.find(|check| {
check["name"] == "dispatcher_local_builds"
|| check["check_name"] == "dispatcher_local_builds"
})
.expect("public doctor path must include local-build diagnostic");
assert_attribution(check["details"].as_str().unwrap());
}
let mut watch = HeldChild(
cli(true)
.args([
"doctor",
"--reliability",
"--scope=triage",
"--watch",
"--watch-interval=1",
])
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap(),
);
let stdout = watch.0.stdout.take().unwrap();
let (sender, receiver) = std::sync::mpsc::channel();
let reader = std::thread::spawn(move || {
for value in serde_json::Deserializer::from_reader(BufReader::new(stdout))
.into_iter::<serde_json::Value>()
{
if sender.send(value).is_err() {
break;
}
}
});
for expected_sweep in [1, 2] {
let value = receiver
.recv_timeout(Duration::from_secs(30))
.expect("watch sweep")
.unwrap();
assert_eq!(value["sweep_index"].as_u64(), Some(expected_sweep));
let check = value["response"]["diagnostics"]
.as_array()
.unwrap()
.iter()
.find(|check| check["check_name"] == "dispatcher_local_builds")
.unwrap();
assert_attribution(check["details"].as_str().unwrap());
}
drop(watch);
reader.join().unwrap();
std::fs::write(
config_dir.join("config.toml"),
"[general]\nrole = \"worker\"\n",
)
.unwrap();
let output = cli(true)
.args(["doctor", "--reliability", "--scope=triage"])
.output()
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
!value["data"]["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|check| check["check_name"] == "dispatcher_local_builds")
);
}
#[cfg(target_os = "linux")]
#[test]
fn gc_timeout_streams_progress_reaps_ssh_and_emits_one_machine_response() {
use std::io::{BufRead, BufReader, Read};
use std::os::unix::fs::PermissionsExt;
use std::process::{Child, Stdio};
use std::time::{Duration, Instant};
struct GcChild(Child);
impl Drop for GcChild {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
let temp = tempfile::tempdir().unwrap();
let bin = temp.path().join("bin");
let config = temp.path().join("config");
std::fs::create_dir_all(&bin).unwrap();
std::fs::create_dir_all(&config).unwrap();
let ssh = bin.join("ssh");
std::fs::write(
&ssh,
"#!/bin/sh\n\
for arg do\n\
case \"$arg\" in\n\
*fixture-slow*)\n\
if [ \"$RCH_GC_FIXTURE_APPLY\" = 1 ]; then\n\
case \"$*\" in\n\
*RCH_TARGET_ENTRY*)\n\
printf 'RCH_GC_ROOT resolved /tmp\\nRCH_GC_ROOT tmp /tmp\\nRCH_GC_GATES handles=1 handles_source=proc procs=1\\n'\n\
i=0\n\
while [ \"$i\" -le 100 ]; do\n\
printf 'RCH_TARGET_ENTRY 1 10 free free /tmp/rch_target_fixture_%s\\n' \"$i\"\n\
i=$((i + 1))\n\
done\n\
exit 0\n\
;;\n\
esac\n\
if [ ! -e \"$RCH_GC_FIXTURE_BATCH\" ]; then\n\
printf 'completed\\n' > \"$RCH_GC_FIXTURE_BATCH\"\n\
i=0\n\
while [ \"$i\" -lt 100 ]; do\n\
printf 'RCH_REAP_RM 10 ttl /tmp/rch_target_fixture_%s\\n' \"$i\"\n\
i=$((i + 1))\n\
done\n\
printf 'RCH_WORKER_REAP_METRICS removed=100 freed_kb=1000\\n'\n\
exit 0\n\
fi\n\
fi\n\
printf '%s\\n' \"$$\" > \"$RCH_GC_FIXTURE_PID_FILE\"\n\
exec /bin/sleep 6\n\
;;\n\
esac\n\
done\n\
printf 'controlled SSH connection failure\\n' >&2\n\
exit 255\n",
)
.unwrap();
std::fs::set_permissions(&ssh, std::fs::Permissions::from_mode(0o700)).unwrap();
std::fs::write(
config.join("config.toml"),
"[self_healing]\nhook_starts_daemon = false\ndaemon_installs_hooks = false\n",
)
.unwrap();
std::fs::write(
config.join("workers.toml"),
"[[workers]]\nid = \"slow\"\nhost = \"fixture-slow.invalid\"\nuser = \"fixture\"\nidentity_file = \"missing\"\n\
[[workers]]\nid = \"fast\"\nhost = \"fixture-fast.invalid\"\nuser = \"fixture\"\nidentity_file = \"missing\"\n",
)
.unwrap();
let path = std::env::join_paths(std::iter::once(bin).chain(std::env::split_paths(
&std::env::var_os("PATH").unwrap_or_default(),
)))
.unwrap();
for (format, apply) in [
("json", false),
("toon", false),
("json", true),
("toon", true),
] {
let pid_file = temp.path().join(format!("ssh-{format}-{apply}.pid"));
let batch_file = temp.path().join(format!("batch-{format}-{apply}"));
let mut child = GcChild(
Command::new(env!("CARGO_BIN_EXE_rch"))
.current_dir(temp.path())
.env("HOME", temp.path())
.env("XDG_CACHE_HOME", temp.path().join("cache"))
.env("XDG_CONFIG_HOME", temp.path().join("xdg-config"))
.env("RCH_CONFIG_DIR", &config)
.env("RCH_GC_FIXTURE_PID_FILE", &pid_file)
.env("RCH_GC_FIXTURE_APPLY", if apply { "1" } else { "0" })
.env("RCH_GC_FIXTURE_BATCH", batch_file)
.env("PATH", &path)
.env("NO_COLOR", "1")
.env("RCH_LOG_LEVEL", "error")
.env_remove("RUST_LOG")
.env_remove("RCH_LOG_FILE")
.env_remove("RCH_JSON")
.env_remove("RCH_OUTPUT_FORMAT")
.env_remove("TOON_DEFAULT_FORMAT")
.args([
"--no-self-healing",
"--json",
&format!("--format={format}"),
"gc",
"--worker-timeout=2",
])
.args(apply.then_some("--apply"))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap(),
);
let stdout = child.0.stdout.take().unwrap();
let stdout_reader = std::thread::spawn(move || {
let mut text = String::new();
BufReader::new(stdout).read_to_string(&mut text).unwrap();
text
});
let stderr = child.0.stderr.take().unwrap();
let (sender, receiver) = std::sync::mpsc::channel();
let stderr_reader = std::thread::spawn(move || {
let mut text = String::new();
for line in BufReader::new(stderr).lines() {
let line = line.unwrap();
let _ = sender.send(line.clone());
text.push_str(&line);
text.push('\n');
}
text
});
let deadline = Instant::now() + Duration::from_secs(12);
let mut fast_finished_while_running = false;
while let Ok(line) = receiver.recv_timeout(Duration::from_secs(10)) {
if line.starts_with("[rch gc] fast: failed:") {
fast_finished_while_running = child.0.try_wait().unwrap().is_none();
break;
}
if Instant::now() >= deadline {
break;
}
}
let status = loop {
if let Some(status) = child.0.try_wait().unwrap() {
break Some(status);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(10));
};
drop(child); let stdout = stdout_reader.join().unwrap();
let stderr = stderr_reader.join().unwrap();
let pid = std::fs::read_to_string(&pid_file)
.unwrap_or_else(|error| {
panic!(
"slow SSH fixture missing ({format}, apply={apply}): {error}; status={status:?}; stdout={stdout}; stderr={stderr}"
)
})
.trim()
.parse::<u32>()
.unwrap();
let process_path = std::path::PathBuf::from(format!("/proc/{pid}"));
let ssh_was_reaped = !process_path.exists();
let cleanup_deadline = Instant::now() + Duration::from_secs(7);
while !ssh_was_reaped && process_path.exists() && Instant::now() < cleanup_deadline {
std::thread::sleep(Duration::from_millis(25));
}
assert!(status.is_some(), "GC exceeded its watchdog: {stderr}");
assert_eq!(status.unwrap().code(), Some(1), "{stderr}");
assert!(fast_finished_while_running, "buffered progress: {stderr}");
let fast = stderr.find("[rch gc] fast: failed:").unwrap();
let slow = stderr.find("[rch gc] slow: timed out:").unwrap();
assert!(fast < slow, "slow worker blocked fast worker: {stderr}");
assert!(ssh_was_reaped, "GC returned with SSH PID {pid} still alive");
let json = if format == "toon" {
toon_rust::toon_to_json(&stdout).unwrap()
} else {
stdout.clone()
};
let value: serde_json::Value = serde_json::from_str(&json)
.unwrap_or_else(|error| panic!("invalid {format} response: {error}: {stdout}"));
assert_eq!(value["success"], false);
assert_eq!(value["command"], "gc");
assert_eq!(value["error"]["code"], "RCH-E104");
assert_eq!(value["data"]["dry_run"], !apply);
assert_eq!(value["data"]["applied"], apply);
let workers = value["data"]["workers"].as_array().unwrap();
assert_eq!(workers.len(), 2);
let slow = workers.iter().find(|row| row["id"] == "slow").unwrap();
let fast = workers.iter().find(|row| row["id"] == "fast").unwrap();
assert_eq!(slow["timed_out"], true);
assert_eq!(slow["apply_outcome_unknown"], apply);
assert_eq!(slow["phase"], if apply { "collect" } else { "enumerate" });
if apply {
assert_eq!(slow["removed"].as_f64(), Some(100.0));
assert_eq!(slow["freed_kb"].as_f64(), Some(1000.0));
assert_eq!(slow["entries"].as_array().unwrap().len(), 100);
assert_eq!(
slow["unknown_batch_paths"],
serde_json::json!(["/tmp/rch_target_fixture_100"])
);
assert!(stderr.contains("unknown"), "{stderr}");
assert!(stderr.contains("100 confirmed"), "{stderr}");
}
assert_eq!(fast["timed_out"], false);
assert_eq!(fast["ok"], false);
}
}
#[test]
fn test_rch_help_includes_description() {
init_test_logging();
crate::test_log!("TEST START: test_rch_help_includes_description");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--help")
.output()
.expect("Failed to run rch --help");
assert!(output.status.success(), "rch --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "Remote Compilation Helper");
crate::test_log!("TEST PASS: test_rch_help_includes_description");
}
#[test]
fn test_rch_version_output() {
init_test_logging();
crate::test_log!("TEST START: test_rch_version_output");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--version")
.output()
.expect("Failed to run rch --version");
assert!(output.status.success(), "rch --version failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "rch");
crate::test_log!("Version output: {}", stdout.trim());
crate::test_log!("TEST PASS: test_rch_version_output");
}
#[test]
fn test_exec_refuses_non_compilation_local_fallback_when_remote_required() {
init_test_logging();
crate::test_log!(
"TEST START: test_exec_refuses_non_compilation_local_fallback_when_remote_required"
);
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.env("RCH_REQUIRE_REMOTE", "1")
.args(["exec", "--", "echo", "should_not_run_locally"])
.output()
.expect("Failed to run rch exec non-compilation command");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"RCH_REQUIRE_REMOTE=1 must fail closed for non-compilation exec commands"
);
assert!(
stdout.trim().is_empty(),
"non-compilation command must not execute locally; stdout={stdout:?}"
);
assert_contains(
&stderr,
"remote required; refusing local fallback [RCH-E301] (non-compilation command)",
);
crate::test_log!(
"TEST PASS: test_exec_refuses_non_compilation_local_fallback_when_remote_required"
);
}
#[test]
fn test_exec_refuses_shell_wrapped_cargo_without_local_fallback() {
init_test_logging();
crate::test_log!("TEST START: test_exec_refuses_shell_wrapped_cargo_without_local_fallback");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["exec", "--", "sh", "-c", "cargo build --release"])
.output()
.expect("Failed to run rch exec shell-wrapped cargo command");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"shell-wrapped cargo must fail before either remote execution or local fallback"
);
assert!(stdout.trim().is_empty(), "stdout={stdout:?}");
assert_contains(&stderr, "[RCH-E301] refusing shell-wrapped cargo command");
assert_contains(&stderr, "invoke `rch exec -- cargo ...` directly");
crate::test_log!("TEST PASS: test_exec_refuses_shell_wrapped_cargo_without_local_fallback");
}
#[test]
fn test_daemon_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_daemon_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["daemon", "--help"])
.output()
.expect("Failed to run rch daemon --help");
assert!(output.status.success(), "rch daemon --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "daemon");
crate::test_log!("TEST PASS: test_daemon_subcommand_help");
}
#[test]
fn test_workers_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_workers_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["workers", "--help"])
.output()
.expect("Failed to run rch workers --help");
assert!(output.status.success(), "rch workers --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "workers");
crate::test_log!("TEST PASS: test_workers_subcommand_help");
}
#[test]
fn test_config_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_config_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["config", "--help"])
.output()
.expect("Failed to run rch config --help");
assert!(output.status.success(), "rch config --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "config");
crate::test_log!("TEST PASS: test_config_subcommand_help");
}
#[test]
fn test_hook_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_hook_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["hook", "--help"])
.output()
.expect("Failed to run rch hook --help");
assert!(output.status.success(), "rch hook --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "hook");
crate::test_log!("TEST PASS: test_hook_subcommand_help");
}
#[test]
fn test_status_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_status_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["status", "--help"])
.output()
.expect("Failed to run rch status --help");
assert!(output.status.success(), "rch status --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "status");
crate::test_log!("TEST PASS: test_status_subcommand_help");
}
#[test]
fn test_diagnose_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_diagnose_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["diagnose", "--help"])
.output()
.expect("Failed to run rch diagnose --help");
assert!(output.status.success(), "rch diagnose --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "diagnose");
crate::test_log!("TEST PASS: test_diagnose_subcommand_help");
}
#[test]
fn test_doctor_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_doctor_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["doctor", "--help"])
.output()
.expect("Failed to run rch doctor --help");
assert!(output.status.success(), "rch doctor --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "doctor");
assert_contains(&stdout, "--reliability");
assert_contains(&stdout, "--check-schemas");
crate::test_log!("TEST PASS: test_doctor_subcommand_help");
}
#[test]
fn test_doctor_reliability_json_outputs_real_binary_response() {
init_test_logging();
crate::test_log!("TEST START: test_doctor_reliability_json_outputs_real_binary_response");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["doctor", "--reliability", "--check-schemas", "--json"])
.output()
.expect("Failed to run rch doctor --reliability --check-schemas --json");
let parsed: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("reliability doctor should output JSON");
assert_eq!(
parsed.pointer("/success").and_then(|value| value.as_bool()),
Some(true)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let overall = parsed
.pointer("/data/summary/overall")
.and_then(|value| value.as_str())
.expect("reliability doctor should report an overall verdict");
let expected_exit = match overall {
"healthy" => Some(0),
"degraded" => Some(1),
"failing" => Some(2),
_ => None,
};
assert!(
expected_exit.is_some(),
"unexpected reliability verdict: {overall}"
);
assert_eq!(
output.status.code(),
expected_exit,
"rch doctor --reliability should use the documented exit code for verdict {overall}; stdout:\n{stdout}\nstderr:\n{stderr}"
);
assert_eq!(
parsed
.pointer("/data/schema_version")
.and_then(|value| value.as_str()),
Some("1.0.0")
);
assert_eq!(
parsed
.pointer("/data/mode")
.and_then(|value| value.as_str()),
Some("check")
);
let diagnostics = parsed
.pointer("/data/diagnostics")
.and_then(|value| value.as_array())
.expect("reliability doctor data should include diagnostics");
assert!(
!diagnostics.is_empty(),
"reliability doctor should report at least one diagnostic"
);
assert!(
parsed
.pointer("/data/remediation_plan")
.and_then(|value| value.as_array())
.is_some(),
"reliability doctor data should include remediation_plan"
);
let categories = diagnostics
.iter()
.filter_map(|diagnostic| diagnostic.get("category").and_then(|value| value.as_str()))
.collect::<Vec<_>>();
for expected in [
"topology",
"repo_presence",
"disk_pressure",
"process_debt",
"helper_compatibility",
"rollout_posture",
"schema_compatibility",
] {
assert!(
categories.contains(&expected),
"missing reliability category {expected}; categories={categories:?}"
);
}
crate::test_log!("TEST PASS: test_doctor_reliability_json_outputs_real_binary_response");
}
#[test]
fn test_doctor_handles_closed_stdout_pipe() {
init_test_logging();
crate::test_log!("TEST START: test_doctor_handles_closed_stdout_pipe");
let output = Command::new("bash")
.args([
"-o",
"pipefail",
"-c",
"\"$RCH_BIN\" doctor 2>&1 | head -20",
])
.env("RCH_BIN", env!("CARGO_BIN_EXE_rch"))
.output()
.expect("Failed to run rch doctor pipe regression");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"rch doctor should exit cleanly when stdout closes early; status={:?}\nstdout:\n{}\nstderr:\n{}",
output.status.code(),
stdout,
stderr
);
assert_contains(&stdout, "RCH Diagnostic Report");
assert!(
!stderr.contains("panicked") && !stderr.contains("core dumped"),
"rch doctor should not report a panic or abort when piped to head; stderr:\n{stderr}"
);
crate::test_log!("TEST PASS: test_doctor_handles_closed_stdout_pipe");
}
#[test]
fn test_speedscore_subcommand_help() {
init_test_logging();
crate::test_log!("TEST START: test_speedscore_subcommand_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["speedscore", "--help"])
.output()
.expect("Failed to run rch speedscore --help");
assert!(output.status.success(), "rch speedscore --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "speedscore");
crate::test_log!("TEST PASS: test_speedscore_subcommand_help");
}
#[test]
fn test_invalid_subcommand_fails() {
init_test_logging();
crate::test_log!("TEST START: test_invalid_subcommand_fails");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("nonexistent-command")
.output()
.expect("Failed to run rch nonexistent-command");
assert!(
!output.status.success(),
"Expected failure for invalid subcommand"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("error") || stderr.contains("unrecognized"),
"Expected error message in stderr: {}",
stderr
);
crate::test_log!("TEST PASS: test_invalid_subcommand_fails");
}
#[test]
fn test_invalid_flag_fails() {
init_test_logging();
crate::test_log!("TEST START: test_invalid_flag_fails");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--nonexistent-flag")
.output()
.expect("Failed to run rch --nonexistent-flag");
assert!(
!output.status.success(),
"Expected failure for invalid flag"
);
crate::test_log!("TEST PASS: test_invalid_flag_fails");
}
#[test]
fn test_global_verbose_flag_accepted() {
init_test_logging();
crate::test_log!("TEST START: test_global_verbose_flag_accepted");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--verbose", "--help"])
.output()
.expect("Failed to run rch --verbose --help");
assert!(output.status.success(), "rch --verbose --help failed");
crate::test_log!("TEST PASS: test_global_verbose_flag_accepted");
}
#[test]
fn test_global_quiet_flag_accepted() {
init_test_logging();
crate::test_log!("TEST START: test_global_quiet_flag_accepted");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--quiet", "--help"])
.output()
.expect("Failed to run rch --quiet --help");
assert!(output.status.success(), "rch --quiet --help failed");
crate::test_log!("TEST PASS: test_global_quiet_flag_accepted");
}
#[test]
fn test_global_json_flag_accepted() {
init_test_logging();
crate::test_log!("TEST START: test_global_json_flag_accepted");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--json", "--help"])
.output()
.expect("Failed to run rch --json --help");
assert!(output.status.success(), "rch --json --help failed");
crate::test_log!("TEST PASS: test_global_json_flag_accepted");
}
#[test]
fn test_global_color_flag_accepted() -> Result<(), Box<dyn std::error::Error>> {
init_test_logging();
crate::test_log!("TEST START: test_global_color_flag_accepted");
for mode in ["auto", "always", "never"] {
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--color", mode, "--help"])
.output()?;
assert!(
output.status.success(),
"rch --color {} --help failed",
mode
);
}
crate::test_log!("TEST PASS: test_global_color_flag_accepted");
Ok(())
}
#[test]
fn test_global_format_flag_accepted() -> Result<(), Box<dyn std::error::Error>> {
init_test_logging();
crate::test_log!("TEST START: test_global_format_flag_accepted");
for format in ["json", "toon"] {
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--format", format, "--help"])
.output()?;
assert!(
output.status.success(),
"rch --format {} --help failed",
format
);
}
crate::test_log!("TEST PASS: test_global_format_flag_accepted");
Ok(())
}
#[test]
fn test_diagnose_cargo_build_command() {
init_test_logging();
crate::test_log!("TEST START: test_diagnose_cargo_build_command");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["diagnose", "cargo", "build", "--release"])
.output()
.expect("Failed to run rch diagnose cargo build --release");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
crate::test_log!("stdout: {}", stdout);
crate::test_log!("stderr: {}", stderr);
crate::test_log!("TEST PASS: test_diagnose_cargo_build_command");
}
#[test]
fn test_diagnose_quoted_command() {
init_test_logging();
crate::test_log!("TEST START: test_diagnose_quoted_command");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["diagnose", "cargo build --release"])
.output()
.expect("Failed to run rch diagnose 'cargo build --release'");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
crate::test_log!("stdout: {}", stdout);
crate::test_log!("stderr: {}", stderr);
crate::test_log!("TEST PASS: test_diagnose_quoted_command");
}
#[test]
fn test_diagnose_non_compilation_command() {
init_test_logging();
crate::test_log!("TEST START: test_diagnose_non_compilation_command");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["diagnose", "ls", "-la"])
.output()
.expect("Failed to run rch diagnose ls -la");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
crate::test_log!("stdout: {}", stdout);
crate::test_log!("stderr: {}", stderr);
crate::test_log!("TEST PASS: test_diagnose_non_compilation_command");
}
#[test]
fn test_diagnose_json_output() {
init_test_logging();
crate::test_log!("TEST START: test_diagnose_json_output");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--json", "diagnose", "cargo", "build"])
.output()
.expect("Failed to run rch --json diagnose cargo build");
let stdout = String::from_utf8_lossy(&output.stdout);
crate::test_log!("JSON output: {}", stdout);
if !stdout.trim().is_empty() {
let trimmed = stdout.trim();
assert!(
trimmed.starts_with('{') || trimmed.starts_with('['),
"Expected JSON output to start with {{ or [, got: {}",
&trimmed[..trimmed.len().min(100)]
);
}
crate::test_log!("TEST PASS: test_diagnose_json_output");
}
#[test]
fn test_workers_list_help() {
init_test_logging();
crate::test_log!("TEST START: test_workers_list_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["workers", "list", "--help"])
.output()
.expect("Failed to run rch workers list --help");
assert!(output.status.success(), "rch workers list --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "list");
crate::test_log!("TEST PASS: test_workers_list_help");
}
#[test]
fn test_workers_probe_help() {
init_test_logging();
crate::test_log!("TEST START: test_workers_probe_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["workers", "probe", "--help"])
.output()
.expect("Failed to run rch workers probe --help");
assert!(output.status.success(), "rch workers probe --help failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "probe");
crate::test_log!("TEST PASS: test_workers_probe_help");
}
#[test]
fn test_workers_capabilities_help() {
init_test_logging();
crate::test_log!("TEST START: test_workers_capabilities_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["workers", "capabilities", "--help"])
.output()
.expect("Failed to run rch workers capabilities --help");
assert!(
output.status.success(),
"rch workers capabilities --help failed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "capabilities");
crate::test_log!("TEST PASS: test_workers_capabilities_help");
}
#[test]
fn test_daemon_start_help() {
init_test_logging();
crate::test_log!("TEST START: test_daemon_start_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["daemon", "start", "--help"])
.output()
.expect("Failed to run rch daemon start --help");
assert!(output.status.success(), "rch daemon start --help failed");
crate::test_log!("TEST PASS: test_daemon_start_help");
}
#[test]
fn test_daemon_status_help() {
init_test_logging();
crate::test_log!("TEST START: test_daemon_status_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["daemon", "status", "--help"])
.output()
.expect("Failed to run rch daemon status --help");
assert!(output.status.success(), "rch daemon status --help failed");
crate::test_log!("TEST PASS: test_daemon_status_help");
}
#[test]
fn test_config_show_help() {
init_test_logging();
crate::test_log!("TEST START: test_config_show_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["config", "show", "--help"])
.output()
.expect("Failed to run rch config show --help");
assert!(output.status.success(), "rch config show --help failed");
crate::test_log!("TEST PASS: test_config_show_help");
}
#[test]
fn test_config_validate_help() {
init_test_logging();
crate::test_log!("TEST START: test_config_validate_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["config", "validate", "--help"])
.output()
.expect("Failed to run rch config validate --help");
assert!(output.status.success(), "rch config validate --help failed");
crate::test_log!("TEST PASS: test_config_validate_help");
}
#[test]
fn test_error_list_unknown_category_fails() {
init_test_logging();
crate::test_log!("TEST START: test_error_list_unknown_category_fails");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["error", "list", "--category", "nonexistent_category"])
.output()
.expect("Failed to run rch error list");
assert_eq!(
output.status.code(),
Some(2),
"unknown category should be a usage error"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_contains(&stderr, "Unknown error category");
assert_contains(&stderr, "disk_pressure");
crate::test_log!("TEST PASS: test_error_list_unknown_category_fails");
}
#[test]
fn test_error_list_unknown_category_json_fails_with_remediation() {
init_test_logging();
crate::test_log!("TEST START: test_error_list_unknown_category_json_fails_with_remediation");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args([
"error",
"list",
"--category",
"nonexistent_category",
"--json",
])
.output()
.expect("Failed to run rch error list --json");
assert_eq!(
output.status.code(),
Some(2),
"unknown category should be a usage error"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(&stdout, "\"success\": false");
assert_contains(&stdout, "\"known_categories\"");
assert_contains(&stdout, "nonexistent_category");
crate::test_log!("TEST PASS: test_error_list_unknown_category_json_fails_with_remediation");
}
#[test]
fn test_hook_install_help() {
init_test_logging();
crate::test_log!("TEST START: test_hook_install_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["hook", "install", "--help"])
.output()
.expect("Failed to run rch hook install --help");
assert!(output.status.success(), "rch hook install --help failed");
crate::test_log!("TEST PASS: test_hook_install_help");
}
#[test]
fn test_hook_uninstall_help() {
init_test_logging();
crate::test_log!("TEST START: test_hook_uninstall_help");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["hook", "uninstall", "--help"])
.output()
.expect("Failed to run rch hook uninstall --help");
assert!(output.status.success(), "rch hook uninstall --help failed");
crate::test_log!("TEST PASS: test_hook_uninstall_help");
}
#[test]
fn test_short_verbose_flag() {
init_test_logging();
crate::test_log!("TEST START: test_short_verbose_flag");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["-v", "--help"])
.output()
.expect("Failed to run rch -v --help");
assert!(output.status.success(), "rch -v --help failed");
crate::test_log!("TEST PASS: test_short_verbose_flag");
}
#[test]
fn test_short_quiet_flag() {
init_test_logging();
crate::test_log!("TEST START: test_short_quiet_flag");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["-q", "--help"])
.output()
.expect("Failed to run rch -q --help");
assert!(output.status.success(), "rch -q --help failed");
crate::test_log!("TEST PASS: test_short_quiet_flag");
}
#[test]
fn test_rch_verbose_env_var() {
init_test_logging();
crate::test_log!("TEST START: test_rch_verbose_env_var");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.env("RCH_VERBOSE", "true")
.arg("--help")
.output()
.expect("Failed to run rch with RCH_VERBOSE=true");
assert!(
output.status.success(),
"rch with RCH_VERBOSE=true --help failed"
);
crate::test_log!("TEST PASS: test_rch_verbose_env_var");
}
#[test]
fn test_rch_output_format_env_var() {
init_test_logging();
crate::test_log!("TEST START: test_rch_output_format_env_var");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.env("RCH_OUTPUT_FORMAT", "json")
.arg("--help")
.output()
.expect("Failed to run rch with RCH_OUTPUT_FORMAT=json");
assert!(
output.status.success(),
"rch with RCH_OUTPUT_FORMAT=json --help failed"
);
crate::test_log!("TEST PASS: test_rch_output_format_env_var");
}
#[test]
fn test_help_json_outputs_valid_json() {
init_test_logging();
crate::test_log!("TEST START: test_help_json_outputs_valid_json");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--help-json")
.output()
.expect("Failed to run rch --help-json");
assert!(output.status.success(), "rch --help-json failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--help-json should output valid JSON");
assert!(parsed.get("name").is_some(), "Missing 'name' field");
assert!(
parsed.get("subcommands").is_some(),
"Missing 'subcommands' field"
);
assert!(parsed.get("version").is_some(), "Missing 'version' field");
crate::test_log!("TEST PASS: test_help_json_outputs_valid_json");
}
#[test]
fn test_help_json_with_subcommand() {
init_test_logging();
crate::test_log!("TEST START: test_help_json_with_subcommand");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--help-json", "workers"])
.output()
.expect("Failed to run rch --help-json workers");
assert!(output.status.success(), "rch --help-json workers failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--help-json workers should output valid JSON");
assert_eq!(
parsed.get("name").and_then(|v| v.as_str()),
Some("workers"),
"Should be 'workers' subcommand"
);
let subcommands = parsed.get("subcommands").and_then(|v| v.as_array());
assert!(subcommands.is_some(), "workers should have subcommands");
assert!(
!subcommands.unwrap().is_empty(),
"workers should have subcommands"
);
crate::test_log!("TEST PASS: test_help_json_with_subcommand");
}
#[test]
fn test_help_json_nested_subcommand_includes_arguments() {
init_test_logging();
crate::test_log!("TEST START: test_help_json_nested_subcommand_includes_arguments");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--help-json", "workers/list"])
.output()
.expect("Failed to run rch --help-json workers/list");
assert!(
output.status.success(),
"rch --help-json workers/list failed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--help-json workers/list should output valid JSON");
assert_eq!(
parsed.get("name").and_then(|v| v.as_str()),
Some("list"),
"Should be the workers list subcommand"
);
let arg_names: Vec<&str> = parsed
.get("arguments")
.and_then(|v| v.as_array())
.expect("nested help should include an arguments array")
.iter()
.filter_map(|arg| arg.get("name").and_then(|v| v.as_str()))
.collect();
assert!(
arg_names.contains(&"speedscore"),
"workers/list help-json should expose the --speedscore flag"
);
crate::test_log!("TEST PASS: test_help_json_nested_subcommand_includes_arguments");
}
#[test]
fn test_help_json_space_separated_nested_subcommand_path() {
init_test_logging();
crate::test_log!("TEST START: test_help_json_space_separated_nested_subcommand_path");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--help-json", "workers", "list"])
.output()
.expect("Failed to run rch --help-json workers list");
assert!(
output.status.success(),
"rch --help-json workers list failed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--help-json workers list should output valid JSON");
assert_eq!(parsed.get("name").and_then(|v| v.as_str()), Some("list"));
crate::test_log!("TEST PASS: test_help_json_space_separated_nested_subcommand_path");
}
#[test]
fn test_help_json_resolves_subcommand_alias() {
init_test_logging();
crate::test_log!("TEST START: test_help_json_resolves_subcommand_alias");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--help-json", "tui"])
.output()
.expect("Failed to run rch --help-json tui");
assert!(output.status.success(), "rch --help-json tui failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--help-json tui should output valid JSON");
assert_eq!(
parsed.get("name").and_then(|v| v.as_str()),
Some("dashboard")
);
crate::test_log!("TEST PASS: test_help_json_resolves_subcommand_alias");
}
#[test]
fn test_capabilities_outputs_valid_json() {
init_test_logging();
crate::test_log!("TEST START: test_capabilities_outputs_valid_json");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--capabilities")
.output()
.expect("Failed to run rch --capabilities");
assert!(output.status.success(), "rch --capabilities failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--capabilities should output valid JSON");
assert!(parsed.get("version").is_some(), "Missing 'version' field");
assert!(parsed.get("runtimes").is_some(), "Missing 'runtimes' field");
assert!(parsed.get("commands").is_some(), "Missing 'commands' field");
assert!(parsed.get("features").is_some(), "Missing 'features' field");
crate::test_log!("TEST PASS: test_capabilities_outputs_valid_json");
}
#[test]
fn test_capabilities_lists_supported_runtimes() {
init_test_logging();
crate::test_log!("TEST START: test_capabilities_lists_supported_runtimes");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--capabilities")
.output()
.expect("Failed to run rch --capabilities");
assert!(output.status.success(), "rch --capabilities failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let runtimes = parsed.get("runtimes").and_then(|v| v.as_array()).unwrap();
let runtime_names: Vec<&str> = runtimes
.iter()
.filter_map(|r| r.get("name").and_then(|n| n.as_str()))
.collect();
assert!(
runtime_names.contains(&"rust"),
"Should support rust runtime"
);
assert!(runtime_names.contains(&"bun"), "Should support bun runtime");
assert!(
runtime_names.contains(&"node"),
"Should support node runtime"
);
crate::test_log!("TEST PASS: test_capabilities_lists_supported_runtimes");
}
#[test]
fn test_capabilities_lists_all_commands() {
init_test_logging();
crate::test_log!("TEST START: test_capabilities_lists_all_commands");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.arg("--capabilities")
.output()
.expect("Failed to run rch --capabilities");
assert!(output.status.success(), "rch --capabilities failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let commands = parsed.get("commands").and_then(|v| v.as_array()).unwrap();
let command_names: Vec<&str> = commands
.iter()
.filter_map(|c| c.get("name").and_then(|n| n.as_str()))
.collect();
assert!(command_names.contains(&"init"), "Should list init command");
assert!(
command_names.contains(&"daemon"),
"Should list daemon command"
);
assert!(
command_names.contains(&"workers"),
"Should list workers command"
);
assert!(
command_names.contains(&"status"),
"Should list status command"
);
assert!(
command_names.contains(&"config"),
"Should list config command"
);
crate::test_log!("TEST PASS: test_capabilities_lists_all_commands");
}
#[test]
fn test_capabilities_command_outputs_api_envelope() {
init_test_logging();
crate::test_log!("TEST START: test_capabilities_command_outputs_api_envelope");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["capabilities", "--json"])
.output()
.expect("Failed to run rch capabilities --json");
assert!(output.status.success(), "rch capabilities --json failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("capabilities --json should output valid JSON");
assert_eq!(parsed.get("success").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
parsed
.pointer("/data/contract_version")
.and_then(|v| v.as_str()),
Some("rch.capabilities.v1")
);
assert!(
parsed
.pointer("/data/env_vars")
.and_then(|v| v.as_array())
.is_some_and(|vars| !vars.is_empty()),
"capabilities should include env var dictionary"
);
assert!(
parsed
.pointer("/data/exit_codes")
.and_then(|v| v.as_array())
.is_some_and(|codes| !codes.is_empty()),
"capabilities should include exit code dictionary"
);
crate::test_log!("TEST PASS: test_capabilities_command_outputs_api_envelope");
}
#[test]
fn test_robot_docs_guide_outputs_agent_handbook() {
init_test_logging();
crate::test_log!("TEST START: test_robot_docs_guide_outputs_agent_handbook");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["robot-docs", "guide"])
.output()
.expect("Failed to run rch robot-docs guide");
assert!(output.status.success(), "rch robot-docs guide failed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("RCH Agent Guide"));
assert!(stdout.contains("rch capabilities --json"));
assert!(stdout.contains("rch --robot-triage --json"));
crate::test_log!("TEST PASS: test_robot_docs_guide_outputs_agent_handbook");
}
#[test]
fn test_robot_docs_guide_json_outputs_api_envelope() {
init_test_logging();
crate::test_log!("TEST START: test_robot_docs_guide_json_outputs_api_envelope");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["robot-docs", "guide", "--json"])
.output()
.expect("Failed to run rch robot-docs guide --json");
assert!(
output.status.success(),
"rch robot-docs guide --json failed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("robot-docs guide --json should be valid JSON");
assert_eq!(parsed.get("success").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
parsed
.pointer("/data/contract_version")
.and_then(|v| v.as_str()),
Some("rch.robot_docs.v1")
);
assert!(
parsed
.pointer("/data/guide")
.and_then(|v| v.as_str())
.is_some_and(|guide| guide.contains("RCH Agent Guide"))
);
crate::test_log!("TEST PASS: test_robot_docs_guide_json_outputs_api_envelope");
}
#[test]
fn test_robot_triage_json_outputs_quick_ref() {
init_test_logging();
crate::test_log!("TEST START: test_robot_triage_json_outputs_quick_ref");
let output = Command::new(env!("CARGO_BIN_EXE_rch"))
.args(["--robot-triage", "--json"])
.output()
.expect("Failed to run rch --robot-triage --json");
assert!(output.status.success(), "rch --robot-triage --json failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--robot-triage --json should be valid JSON");
assert_eq!(parsed.get("success").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
parsed
.pointer("/data/contract_version")
.and_then(|v| v.as_str()),
Some("rch.robot_triage.v1")
);
assert_eq!(
parsed
.pointer("/data/quick_ref/default_probe")
.and_then(|v| v.as_str()),
Some("rch check --json")
);
assert!(
parsed
.pointer("/data/recommended_commands")
.and_then(|v| v.as_array())
.is_some_and(|commands| !commands.is_empty())
);
crate::test_log!("TEST PASS: test_robot_triage_json_outputs_quick_ref");
}