use crate::common::*;
use std::process::Command;
use std::time::{Duration, Instant};
#[test]
fn timeout_kills_and_exits_124() {
if !have_landlock() {
eprintln!("SKIP: no tier");
return;
}
let proj = TempProject::new("timeout-kill");
let out = run_vetto_in(
proj.path(),
&["--timeout", "1s", "--tui=none", "--", "sleep", "30"],
);
assert_eq!(
out.status.code(),
Some(124),
"expected GNU-timeout exit code 124; stderr: {}",
stderr(&out)
);
assert!(
stderr(&out).contains("session timeout"),
"stderr must report the session timeout; got: {}",
stderr(&out)
);
}
#[test]
fn timeout_not_triggered_when_agent_finishes() {
if !have_landlock() {
eprintln!("SKIP: no tier");
return;
}
let proj = TempProject::new("timeout-finish");
let out = run_vetto_in(
proj.path(),
&["--timeout", "30s", "--tui=none", "--", "true"],
);
assert!(
out.status.success(),
"agent finishing first must not be killed by the deadline; stderr: {}",
stderr(&out)
);
}
#[test]
fn timeout_event_lands_in_jsonl() {
if !have_landlock() {
eprintln!("SKIP: no tier");
return;
}
let proj = TempProject::new("timeout-jsonl");
let sink = proj.path().join("session.jsonl");
let out = run_vetto_in(
proj.path(),
&[
"--jsonl",
sink.to_str().expect("utf-8 sink path"),
"--timeout",
"1s",
"--tui=none",
"--",
"sleep",
"30",
],
);
assert_eq!(
out.status.code(),
Some(124),
"expected exit 124; stderr: {}",
stderr(&out)
);
let content = std::fs::read_to_string(&sink).expect("jsonl sink file written");
assert!(
content.contains("session_timeout"),
"jsonl must contain the session_timeout event; got:\n{content}"
);
}
#[test]
fn timeout_kills_tree_and_exits_124() {
if !have_landlock() {
eprintln!("SKIP: no tier");
return;
}
let proj = TempProject::new("timeout-tree");
let marker = format!("tree-leaf-{}", std::process::id());
let script = format!("sh -c 'sleep 30 # {marker}' & sh -c 'sleep 30 # {marker}' & sleep 30");
let out = run_vetto_in(
proj.path(),
&["--timeout", "1s", "--tui=none", "--", "sh", "-c", &script],
);
assert_eq!(
out.status.code(),
Some(124),
"expected exit 124 on tree timeout; stderr: {}",
stderr(&out)
);
assert!(
stderr(&out).contains("session timeout"),
"stderr must report session timeout; got: {}",
stderr(&out)
);
let pgrep = Command::new("pgrep")
.args(["-f", &marker])
.output()
.expect("pgrep");
assert!(
pgrep.stdout.is_empty(),
"processes from tree survived timeout kill: {}",
String::from_utf8_lossy(&pgrep.stdout)
);
}
#[test]
fn timeout_with_residual_forces_exit_125() {
use vetto::exit_codes::{map_session_exit_code, EXIT_FAIL_CLOSED, EXIT_TIMEOUT};
use vetto::proctree::{ExtinctionVerifier, PlatformExtinctionTier};
let breach = ExtinctionVerifier::verify(
PlatformExtinctionTier::LinuxTier1Proven,
1, 0,
100,
)
.unwrap_err();
assert_eq!(breach.exit_code, EXIT_FAIL_CLOSED);
assert_eq!(breach.exit_code, 125);
let mapped = map_session_exit_code(breach.exit_code, true, false);
assert_eq!(
mapped, EXIT_FAIL_CLOSED,
"residual extinction breach (125) must dominate timeout (124)"
);
assert_ne!(mapped, EXIT_TIMEOUT);
}
#[test]
fn timeout_signal_escalation_trapped_sigterm_to_sigkill() {
if !have_landlock() {
eprintln!("SKIP: no tier");
return;
}
let proj = TempProject::new("trap-sigterm");
let log_file = proj.path().join("term.log");
let script = format!(
"trap 'echo TRAPPED >> \"{}\"' TERM; while true; do sleep 0.05; done",
log_file.display()
);
let start = Instant::now();
let out = run_vetto_in(
proj.path(),
&["--timeout", "1s", "--tui=none", "--", "sh", "-c", &script],
);
let elapsed = start.elapsed();
assert_eq!(
out.status.code(),
Some(124),
"expected exit 124 after escalation; stderr: {}",
stderr(&out)
);
if log_file.exists() {
let content = std::fs::read_to_string(&log_file).unwrap_or_default();
assert!(
content.contains("TRAPPED"),
"agent should have received and trapped SIGTERM before SIGKILL; got: {content}"
);
}
assert!(
elapsed < Duration::from_secs(6),
"escalation took too long: {:?}",
elapsed
);
}
#[test]
fn timeout_graceful_exit_on_sigterm_avoids_sigkill() {
if !have_landlock() {
eprintln!("SKIP: no tier");
return;
}
let proj = TempProject::new("graceful-sigterm");
let log_file = proj.path().join("exit.log");
let script = format!(
"trap 'echo CLEAN_SHUTDOWN >> \"{}\"; exit 0' TERM; while true; do sleep 0.05; done",
log_file.display()
);
let out = run_vetto_in(
proj.path(),
&["--timeout", "1s", "--tui=none", "--", "sh", "-c", &script],
);
assert_eq!(
out.status.code(),
Some(124),
"expected exit 124 on timeout; stderr: {}",
stderr(&out)
);
let content = std::fs::read_to_string(&log_file).unwrap_or_default();
assert!(
content.contains("CLEAN_SHUTDOWN"),
"agent should have performed clean shutdown on SIGTERM; got: {content}"
);
}