use super::support::*;
use std::time::Duration;
#[test]
fn test_headless_starts_and_ipc_ready() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process.ipc_session().expect("ipc session should run");
assert!(
result.success,
"ipc session should succeed. stdout: {}, stderr: {}",
result.stdout, result.stderr
);
let json = parse_ipc_json(&result);
assert_eq!(
json["pid"].as_u64(),
Some(process.pid as u64),
"session should show correct PID: {}",
result.stdout
);
assert_eq!(
json["ipc_policy"],
serde_json::json!({
"silent": {"mode": "unrestricted"},
"visible": {"mode": "approval_not_required"}
})
);
}
#[test]
fn test_headless_eval_value() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process.ipc_eval("1 + 1").expect("eval should run");
assert!(
result.success,
"eval should succeed. stderr: {}",
result.stderr
);
assert!(
result.stdout.contains("[1] 2"),
"should capture R value: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_allowlist() {
let process = HeadlessProcess::spawn_with_args(&["--ipc-eval-allow-function", "+"])
.expect("Failed to spawn restricted headless session");
let session = process.ipc_session().expect("session should run");
let session_json = parse_ipc_json(&session);
assert_eq!(session_json["ipc_policy"]["silent"]["mode"], "restricted");
assert_eq!(
session_json["ipc_policy"]["silent"]["allowed_functions"],
serde_json::json!(["+"])
);
assert_eq!(
session_json["ipc_policy"]["visible"],
serde_json::json!({"mode": "approval_not_required"})
);
let allowed = process.ipc_eval("1 + 1").expect("allowed eval should run");
assert!(
allowed.success && allowed.stdout.contains("[1] 2"),
"allowlisted operator should succeed: stdout={}, stderr={}",
allowed.stdout,
allowed.stderr
);
let denied = process
.ipc_eval("length('abc')")
.expect("denied eval should return a protocol response");
assert_eq!(denied.exit_code, Some(4));
let error: serde_json::Value = serde_json::from_str(&denied.stderr)
.unwrap_or_else(|e| panic!("denial should be structured JSON: {e}: {}", denied.stderr));
assert_eq!(error["error"]["code"], "R_EVAL_NOT_ALLOWED");
}
#[test]
fn headless_uses_arf_config_for_ipc_eval_policy() {
let config = tempfile::NamedTempFile::new().expect("create config file");
std::fs::write(
config.path(),
r#"[ipc.eval]
allowed_functions = ["length"]
"#,
)
.expect("write config file");
let config_path = config.path().to_string_lossy().into_owned();
let process = HeadlessProcess::spawn_with_args_and_env(
&["--ipc-eval-allow-function", "+"],
&[("ARF_CONFIG", &config_path)],
)
.expect("headless should accept ARF_CONFIG");
let session = process.ipc_session().expect("session should run");
let session_json = parse_ipc_json(&session);
assert_eq!(
session_json["ipc_policy"]["silent"]["allowed_functions"],
serde_json::json!(["+", "length"])
);
}
#[test]
fn test_headless_eval_stdout() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval("cat('hello_headless\\n')")
.expect("eval should run");
assert!(
result.success,
"eval should succeed. stderr: {}",
result.stderr
);
assert!(
result.stdout.contains("hello_headless"),
"should capture stdout: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_error() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval("stop('headless_error')")
.expect("eval should run");
assert!(
result.success,
"eval should succeed (R errors are in JSON, not exit code). stderr: {}",
result.stderr
);
let json = parse_ipc_json(&result);
assert!(
json["error"]
.as_str()
.is_some_and(|s| s.contains("headless_error")),
"should report error in JSON: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_sequential() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let r1 = process.ipc_eval("x <- 42").expect("first eval should run");
assert!(r1.success, "first eval should succeed");
let r2 = process.ipc_eval("x * 2").expect("second eval should run");
assert!(r2.success, "second eval should succeed");
assert!(
r2.stdout.contains("[1] 84"),
"should see variable from first eval: {}",
r2.stdout
);
}
#[test]
fn test_headless_eval_mixed_output() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval("cat('before\\n'); 42")
.expect("eval should run");
assert!(result.success, "eval should succeed");
assert!(
result.stdout.contains("before"),
"should capture stdout: {}",
result.stdout
);
assert!(
result.stdout.contains("[1] 42"),
"should capture value: {}",
result.stdout
);
}
#[test]
fn test_headless_user_input() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_send("invisible(NULL)")
.expect("send should run");
assert!(
result.success,
"send should succeed. stderr: {}",
result.stderr
);
let json = parse_ipc_json(&result);
assert_eq!(
json["accepted"].as_bool(),
Some(true),
"should report acceptance: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_multiline() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let code = r#"f <- function(x) x + 1; f(10)"#;
let result = process.ipc_eval(code).expect("eval should run");
assert!(result.success, "eval should succeed");
assert!(
result.stdout.contains("[1] 11"),
"should evaluate multiline code: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_visible() {
let process = HeadlessProcess::spawn_with_args(&["--ipc-eval-allow-function", "exists"])
.expect("Failed to spawn restricted headless");
let code = r#"cat("vis_marker_42\n")"#;
let result = process
.ipc_eval_visible(code)
.expect("visible eval should run");
assert!(
result.success,
"visible eval should succeed. stderr: {}",
result.stderr
);
assert!(
result.stdout.contains("vis_marker_42"),
"JSON-RPC response should capture stdout: {}",
result.stdout
);
std::thread::sleep(std::time::Duration::from_millis(200));
let server_output = process.server_output();
assert!(
server_output.contains("vis_marker_42"),
"visible eval output should appear on headless process output: {}",
server_output
);
let silent = process
.ipc_eval(code)
.expect("silent eval should return a policy response");
assert_eq!(silent.exit_code, Some(4));
let error = serde_json::from_str::<serde_json::Value>(&silent.stderr)
.expect("silent policy rejection should be structured JSON");
assert_eq!(error["error"]["code"], "R_EVAL_NOT_ALLOWED");
}
#[test]
fn test_headless_eval_silent_no_server_output() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval("cat('silent_marker_99\\n')")
.expect("eval should run");
assert!(result.success, "eval should succeed");
assert!(
result.stdout.contains("silent_marker_99"),
"JSON-RPC response should capture stdout: {}",
result.stdout
);
std::thread::sleep(std::time::Duration::from_millis(200));
let server_output = process.server_output();
assert!(
!server_output.contains("silent_marker_99"),
"silent eval output should NOT appear on headless process output: {}",
server_output
);
}
#[test]
fn test_headless_vanilla_flag() {
let process =
HeadlessProcess::spawn_with_args(&["--vanilla"]).expect("Failed to spawn with --vanilla");
let result = process.ipc_eval("1 + 1").expect("eval should run");
assert!(
result.success,
"eval should succeed with --vanilla. stderr: {}",
result.stderr
);
assert!(
result.stdout.contains("[1] 2"),
"should return result: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_timeout_sufficient() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval_with_timeout("1 + 1", 30000)
.expect("eval with timeout should run");
assert!(result.success, "should succeed: {}", result.stderr);
assert!(
result.stdout.contains("[1] 2"),
"should return result: {}",
result.stdout
);
}
#[test]
fn test_headless_eval_timeout_exceeded() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval_with_timeout("Sys.sleep(10)", 1000)
.expect("eval with timeout should run");
assert!(
!result.success,
"should fail due to timeout. stdout: {}, stderr: {}",
result.stdout, result.stderr
);
assert!(
result.stderr.contains("timed out"),
"should mention timeout: {}",
result.stderr
);
}
#[test]
fn test_headless_shutdown_via_ipc() {
let mut process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let session = process.ipc_session().expect("session should work");
assert!(session.success, "should be running");
let result = process.ipc_shutdown().expect("shutdown should run");
assert!(
result.success,
"shutdown should succeed. stderr: {}",
result.stderr
);
let json = parse_ipc_json(&result);
assert_eq!(
json["accepted"].as_bool(),
Some(true),
"should report acceptance: {}",
result.stdout
);
process
.wait_for_exit(Duration::from_secs(10))
.expect("headless process should exit after shutdown");
}
#[test]
fn test_headless_help_does_not_hang() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval_with_timeout("?mean", 15000)
.expect("help eval should run");
assert!(
result.success,
"help should succeed without hanging. stderr: {}",
result.stderr
);
assert!(
result.stdout.to_lowercase().contains("mean"),
"help output should contain 'mean': {}",
result.stdout
);
}
#[test]
fn test_headless_plot_does_not_hang() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval_with_timeout(
"plot(1:10); dev_name <- names(dev.cur()); dev.off(); cat(dev_name)",
15000,
)
.expect("plot eval should run");
assert!(
result.success,
"plot should succeed without hanging. stderr: {}",
result.stderr
);
let stdout = &result.stdout;
assert!(
stdout.contains("png") || stdout.contains("pdf") || stdout.contains("quartz_off_screen"),
"graphics device should be non-interactive, got: {}",
stdout
);
}
#[test]
fn test_headless_browse_url_does_not_hang() {
let process = HeadlessProcess::spawn().expect("Failed to spawn headless");
let result = process
.ipc_eval_with_timeout("browseURL('https://example.com')", 15000)
.expect("browseURL eval should run");
assert!(
result.success,
"browseURL should succeed without hanging. stderr: {}",
result.stderr
);
assert!(
result.stdout.contains("https://example.com"),
"URL should be captured in stdout: {}",
result.stdout
);
}