mod common;
use common::logging::{TestLogger, TestPhase, init_test_logging};
use std::io::Write;
use std::process::{Command, Stdio};
const ANSI_ESC: &str = "\x1b[";
fn rch_binary() -> String {
std::env::var("RCH_BINARY").unwrap_or_else(|_| env!("CARGO_BIN_EXE_rch").to_string())
}
fn check_binary() -> bool {
let rch = rch_binary();
std::path::Path::new(&rch).exists()
}
macro_rules! require_binary {
() => {
if !check_binary() {
eprintln!("Skipping test: rch binary not found at {}", rch_binary());
eprintln!("Build with: cargo build -p rch --release");
return;
}
};
}
fn run_rch(args: &[&str]) -> (i32, String, String) {
let rch = rch_binary();
let output = Command::new(&rch)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("Failed to run rch");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
fn run_rch_with_env(args: &[&str], env_key: &str, env_val: &str) -> (i32, String, String) {
let rch = rch_binary();
let output = Command::new(&rch)
.args(args)
.env(env_key, env_val)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("Failed to run rch");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
fn run_hook(input: &str) -> (i32, String, String) {
let rch = rch_binary();
let mut child = Command::new(&rch)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn rch");
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(input.as_bytes()).ok();
}
let output = child.wait_with_output().expect("Failed to wait for rch");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
#[test]
fn test_rch_json_env_forces_machine_output_like_the_flag() {
init_test_logging();
let logger = TestLogger::for_test("test_rch_json_env_forces_machine_output_like_the_flag");
require_binary!();
logger.log(TestPhase::Execute, "Running admit under RCH_JSON=1");
let (_exit, stdout, _stderr) = run_rch_with_env(&["admit", "ls -la"], "RCH_JSON", "1");
logger.log(TestPhase::Verify, "Checking the envelope");
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("RCH_JSON=1 must yield a JSON envelope ({e}); got: {stdout}"));
assert_eq!(parsed["api_version"], "1.0", "got: {stdout}");
assert_eq!(parsed["command"], "admit", "got: {stdout}");
assert!(
!stdout.contains(ANSI_ESC),
"machine output must carry no ANSI: {stdout}"
);
logger.log(TestPhase::Execute, "Running admit under RCH_JSON=0");
let (_exit, stdout_off, _stderr) = run_rch_with_env(&["admit", "ls -la"], "RCH_JSON", "0");
assert!(
serde_json::from_str::<serde_json::Value>(&stdout_off).is_err(),
"RCH_JSON=0 must not force the envelope; got: {stdout_off}"
);
logger.pass();
}
#[test]
fn test_status_json_outputs_to_stdout() {
init_test_logging();
let logger = TestLogger::for_test("test_status_json_outputs_to_stdout");
require_binary!();
logger.log(TestPhase::Execute, "Running status --json");
let (_exit, stdout, _stderr) = run_rch(&["status", "--json"]);
logger.log(TestPhase::Verify, "Checking JSON validity");
if !stdout.is_empty() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"status --json stdout must be valid JSON, got: {}",
stdout
);
}
assert!(
!stdout.contains(ANSI_ESC),
"status --json stdout contains ANSI codes: {}",
stdout
);
logger.pass();
}
#[test]
fn test_workers_list_json_outputs_to_stdout() {
init_test_logging();
let logger = TestLogger::for_test("test_workers_list_json_outputs_to_stdout");
require_binary!();
logger.log(TestPhase::Execute, "Running workers list --json");
let (_exit, stdout, _stderr) = run_rch(&["workers", "list", "--json"]);
logger.log(TestPhase::Verify, "Checking JSON validity");
if !stdout.is_empty() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"workers list --json stdout must be valid JSON, got: {}",
stdout
);
}
assert!(
!stdout.contains(ANSI_ESC),
"workers list --json stdout contains ANSI codes: {}",
stdout
);
logger.pass();
}
#[test]
fn test_config_show_json_outputs_to_stdout() {
require_binary!();
let (_exit, stdout, _stderr) = run_rch(&["config", "show", "--json"]);
if !stdout.is_empty() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"config show --json stdout must be valid JSON, got: {}",
stdout
);
}
assert!(
!stdout.contains(ANSI_ESC),
"config show --json stdout contains ANSI codes: {}",
stdout
);
}
#[test]
fn test_hook_stdout_is_clean() {
require_binary!();
let input = r#"{"tool_name":"Bash","tool_input":{"command":"echo hello"}}"#;
let (_exit, stdout, _stderr) = run_hook(input);
if !stdout.is_empty() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"hook stdout must be valid JSON or empty, got: {}",
stdout
);
}
assert!(
!stdout.contains(ANSI_ESC),
"hook stdout contains ANSI codes: {}",
stdout
);
}
#[test]
fn test_hook_cargo_stdout_is_clean() {
require_binary!();
let input = r#"{"tool_name":"Bash","tool_input":{"command":"cargo build"}}"#;
let (_exit, stdout, _stderr) = run_hook(input);
if !stdout.is_empty() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"hook stdout must be valid JSON or empty, got: {}",
stdout
);
}
assert!(
!stdout.contains(ANSI_ESC),
"hook stdout contains ANSI codes: {}",
stdout
);
}
#[test]
fn test_no_color_disables_ansi_in_stdout() {
require_binary!();
let (_exit, stdout, _stderr) = run_rch_with_env(&["status"], "NO_COLOR", "1");
assert!(
!stdout.contains(ANSI_ESC),
"stdout has ANSI codes with NO_COLOR=1: {}",
stdout
);
}
#[test]
fn test_no_color_disables_ansi_in_stderr() {
require_binary!();
let (_exit, _stdout, stderr) = run_rch_with_env(&["status"], "NO_COLOR", "1");
assert!(
!stderr.contains(ANSI_ESC),
"stderr has ANSI codes with NO_COLOR=1: {}",
stderr
);
}
#[test]
fn test_daemon_status_error_to_stderr() {
require_binary!();
let isolated = tempfile::tempdir().expect("isolated daemon status configuration");
let socket = isolated.path().join("absent.sock");
let mut command = Command::new(rch_binary()); command
.args(["daemon", "status"])
.current_dir(isolated.path())
.env("RCH_CONFIG_DIR", isolated.path().join("config"))
.env("RCH_SOCKET_PATH", &socket)
.env("FORCE_COLOR", "1")
.env_remove("NO_COLOR")
.env_remove("RCH_JSON")
.env_remove("RCH_OUTPUT_FORMAT")
.env_remove("RCH_HOOK_MODE");
let output = command.output().expect("run colored daemon status");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "daemon status failed: {stderr}");
assert!(
!stdout.contains(ANSI_ESC),
"daemon status stdout contains ANSI codes: {}",
stdout
);
assert!(
stdout.is_empty(),
"human diagnostics belong on stderr: {stdout}"
);
assert!(
stderr.contains(ANSI_ESC),
"forced color must remain available on stderr: {stderr}"
);
assert!(
stderr.contains("Not running"),
"missing status diagnostic: {stderr}"
);
assert!(
stderr.contains("rch daemon start"),
"missing recovery hint: {stderr}"
);
let json_output = command
.arg("--json")
.output()
.expect("run daemon status JSON");
assert!(json_output.status.success());
let json_stdout = String::from_utf8_lossy(&json_output.stdout);
assert!(
!json_stdout.contains(ANSI_ESC),
"JSON must not contain ANSI: {json_stdout}"
);
let response: serde_json::Value = serde_json::from_str(&json_stdout).unwrap_or_else(|error| {
panic!("daemon status JSON must stay on stdout: {error}: {json_stdout}") });
assert_eq!(response["success"], true);
assert_eq!(response["data"]["running"], false);
assert_eq!(
response["data"]["socket_path"],
socket.to_str().expect("socket path")
);
}
#[test]
fn test_all_json_commands_parseable() {
require_binary!();
let json_commands = [
vec!["status", "--json"],
vec!["workers", "list", "--json"],
vec!["config", "show", "--json"],
];
for args in json_commands {
let (_exit, stdout, _stderr) = run_rch(&args);
if !stdout.is_empty() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"{} output must be valid JSON, got: {}",
args.join(" "),
stdout
);
}
assert!(
!stdout.contains(ANSI_ESC),
"{} stdout contains ANSI codes",
args.join(" ")
);
}
}
#[test]
fn test_stream_isolation_consistent() {
require_binary!();
for _ in 0..3 {
let (_exit, stdout, _stderr) = run_rch(&["status", "--json"]);
assert!(
!stdout.contains(ANSI_ESC),
"stdout contains ANSI codes on repeated run"
);
}
}
#[test]
fn test_stream_isolation_summary() {
require_binary!();
eprintln!("\n=== Stream Isolation Test Summary ===\n");
let json_commands = [
("status --json", vec!["status", "--json"]),
("workers list --json", vec!["workers", "list", "--json"]),
("config show --json", vec!["config", "show", "--json"]),
];
for (name, args) in json_commands {
let (_exit, stdout, _stderr) = run_rch(&args);
let is_valid_json =
stdout.is_empty() || serde_json::from_str::<serde_json::Value>(&stdout).is_ok();
let has_ansi = stdout.contains(ANSI_ESC);
eprintln!(
"{}: valid_json={}, ansi_free={}",
name, is_valid_json, !has_ansi
);
assert!(is_valid_json, "{} must produce valid JSON", name);
assert!(!has_ansi, "{} must not have ANSI in stdout", name);
}
let input = r#"{"tool_name":"Bash","tool_input":{"command":"echo test"}}"#;
let (_exit, stdout, _stderr) = run_hook(input);
let hook_clean = !stdout.contains(ANSI_ESC);
eprintln!("hook: ansi_free={}", hook_clean);
assert!(hook_clean, "hook must not have ANSI in stdout");
let (_exit, stdout, stderr) = run_rch_with_env(&["status"], "NO_COLOR", "1");
let no_color_works = !stdout.contains(ANSI_ESC) && !stderr.contains(ANSI_ESC);
eprintln!("NO_COLOR=1: ansi_free={}", no_color_works);
assert!(no_color_works, "NO_COLOR must disable ANSI everywhere");
eprintln!("\n=== All Stream Isolation Tests PASSED ===\n");
}