use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
fn fresh_config_home(test_name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_nanos();
let home = std::env::temp_dir().join(format!(
"rch-failopen-{test_name}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(home.join("rch")).expect("create test config dir");
home
}
fn run_hook_with_stdin(test_name: &str, stdin_bytes: &[u8]) -> std::process::Output {
let config_home = fresh_config_home(test_name);
let mut child = Command::new(env!("CARGO_BIN_EXE_rch"))
.env("RCH_HOOK_MODE", "1")
.env("RCH_CONFIG_DIR", config_home.join("rch"))
.env("XDG_CONFIG_HOME", &config_home)
.env("RCH_DISABLE_DAEMON_AUTOSTART", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn rch");
{
let stdin = child.stdin.as_mut().expect("rch stdin pipe");
let _ = stdin.write_all(stdin_bytes);
}
drop(child.stdin.take());
child.wait_with_output().expect("wait_with_output")
}
fn assert_fail_open(test_name: &str, output: &std::process::Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(0),
"[{test_name}] exit code must be 0 (any non-zero BLOCKS the agent). \
got status={:?}\nstdout={stdout}\nstderr={stderr}",
output.status
);
assert!(
stdout.is_empty(),
"[{test_name}] stdout must be empty (Allow == empty per protocol). \
got {} bytes: {stdout:?}",
stdout.len()
);
}
#[test]
fn fail_open_garbage_stdin_binary_noise() {
let payload = b"\xFF\xFEnot\x00json\x00\x01\x02";
let out = run_hook_with_stdin("garbage_binary", payload);
assert_fail_open("garbage_binary", &out);
}
#[test]
fn fail_open_empty_stdin() {
let out = run_hook_with_stdin("empty_stdin", b"");
assert_fail_open("empty_stdin", &out);
}
#[test]
fn fail_open_malformed_json_open_brace_only() {
let out = run_hook_with_stdin("malformed_open_brace", b"{");
assert_fail_open("malformed_open_brace", &out);
}
#[test]
fn fail_open_malformed_json_truncated_string() {
let out = run_hook_with_stdin("malformed_truncated_str", b"{\"a\":\"value");
assert_fail_open("malformed_truncated_str", &out);
}
#[test]
fn fail_open_missing_required_fields() {
let out = run_hook_with_stdin("missing_required", b"{\"unrelated\":1}");
assert_fail_open("missing_required", &out);
}
#[test]
fn fail_open_wrong_field_types() {
let payload = b"{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":42}}";
let out = run_hook_with_stdin("wrong_types", payload);
assert_fail_open("wrong_types", &out);
}
#[test]
fn fail_open_json_shape_mismatch_nested_wrong() {
let payload = b"{\"tool_name\":\"Bash\",\"tool_input\":\"not-an-object\"}";
let out = run_hook_with_stdin("shape_mismatch", payload);
assert_fail_open("shape_mismatch", &out);
}
#[test]
fn fail_open_whitespace_only_stdin() {
let out = run_hook_with_stdin("whitespace_only", b" \n\t \n");
assert_fail_open("whitespace_only", &out);
}
#[test]
fn fail_open_unicode_invalid_utf8_bytes() {
let payload = b"\x80\x80\x80abc\xC0\xC0";
let out = run_hook_with_stdin("invalid_utf8", payload);
assert_fail_open("invalid_utf8", &out);
}
#[test]
fn fail_open_null_byte_in_command() {
let payload = b"{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"cargo \\u0000 build\"}}";
let out = run_hook_with_stdin("null_byte_cmd", payload);
assert_fail_open("null_byte_cmd", &out);
}
#[test]
fn fail_open_huge_stdin_within_10mb_cap() {
let payload = vec![b'X'; 1024 * 1024];
let out = run_hook_with_stdin("huge_1mb", &payload);
assert_fail_open("huge_1mb", &out);
}
#[test]
fn fail_open_exceeds_10mb_cap() {
let payload = vec![b'Y'; 11 * 1024 * 1024];
let out = run_hook_with_stdin("huge_11mb", &payload);
assert_fail_open("huge_11mb", &out);
}
#[test]
fn fail_open_array_at_top_level_not_object() {
let out = run_hook_with_stdin("top_array", b"[1,2,3]");
assert_fail_open("top_array", &out);
}
#[test]
fn fail_open_top_level_string_not_object() {
let out = run_hook_with_stdin("top_string", b"\"just a string\"");
assert_fail_open("top_string", &out);
}
#[test]
fn fail_open_top_level_number_not_object() {
let out = run_hook_with_stdin("top_number", b"42");
assert_fail_open("top_number", &out);
}
#[test]
fn fail_open_partial_json_no_closing_brace() {
let payload = br#"{"tool_name":"Bash","tool_input":{"command":"echo hi""#;
let out = run_hook_with_stdin("partial_no_close", payload);
assert_fail_open("partial_no_close", &out);
}