#![allow(clippy::missing_docs_in_private_items)]
use super::runtime::MAX_RUNTIME_SECS;
use super::ttl::MAX_TTL_SECS;
use super::*;
fn never_expires_runtime(_slug: &str) -> u64 {
u64::MAX
}
fn noop_kill(_session: &str) {}
#[test]
fn tmux_kill_session_is_best_effort_on_missing_session() {
tmux_kill_session("moadim-nonexistent-watchdog-test-session");
}
#[test]
fn tmux_bin_falls_back_to_a_nonexistent_path_under_cfg_test() {
let previous = std::env::var_os("MOADIM_TMUX_BIN");
unsafe {
std::env::remove_var("MOADIM_TMUX_BIN");
}
let bin = super::session::tmux_bin();
assert_ne!(bin, "tmux", "test build must not spawn the real tmux");
assert!(
!std::path::Path::new(&bin).exists(),
"test fallback must point at a non-existent path: {bin}"
);
unsafe {
match previous {
Some(value) => std::env::set_var("MOADIM_TMUX_BIN", value),
None => std::env::remove_var("MOADIM_TMUX_BIN"),
}
}
}
#[test]
fn tmux_session_alive_reflects_the_bin_exit_status() {
let previous = std::env::var_os("MOADIM_TMUX_BIN");
let set = |bin: &str| {
unsafe { std::env::set_var("MOADIM_TMUX_BIN", bin) };
};
set("/usr/bin/true");
assert!(
super::session::tmux_session_alive("moadim-anything"),
"a 0-exit tmux stub reads as alive"
);
set("/usr/bin/false");
assert!(
!super::session::tmux_session_alive("moadim-anything"),
"a non-zero-exit tmux stub reads as not alive"
);
unsafe {
match previous {
Some(value) => std::env::set_var("MOADIM_TMUX_BIN", value),
None => std::env::remove_var("MOADIM_TMUX_BIN"),
}
}
}
#[test]
fn note_forced_kill_is_silent_when_log_cannot_be_opened() {
let missing = std::env::temp_dir().join(format!("moadim-nfk-missing-{}", uuid::Uuid::new_v4()));
let _ = std::fs::remove_dir_all(&missing);
super::session::note_forced_kill(&missing);
assert!(!missing.exists());
}
#[test]
fn cleanup_expired_workbenches_kills_a_live_expired_session() {
let home = std::env::temp_dir().join(format!("moadim-cleanup-live-{}", uuid::Uuid::new_v4()));
let prev_home = std::env::var_os("MOADIM_HOME_OVERRIDE");
let prev_tmux = std::env::var_os("MOADIM_TMUX_BIN");
unsafe {
std::env::set_var("MOADIM_HOME_OVERRIDE", &home);
std::env::set_var("MOADIM_TMUX_BIN", "/usr/bin/true");
}
let workbenches = crate::paths::workbenches_dir();
std::fs::create_dir_all(&workbenches).unwrap();
std::fs::create_dir_all(workbenches.join("alive-1")).unwrap();
let store = super::super::model::new_store();
let removed = cleanup_expired_workbenches(&store);
assert!(
removed >= 1,
"the live, expired workbench is killed and reaped"
);
assert!(!workbenches.join("alive-1").exists());
unsafe {
match prev_home {
Some(value) => std::env::set_var("MOADIM_HOME_OVERRIDE", value),
None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
}
match prev_tmux {
Some(value) => std::env::set_var("MOADIM_TMUX_BIN", value),
None => std::env::remove_var("MOADIM_TMUX_BIN"),
}
}
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn tmux_bin_honors_the_override_env_var() {
let previous = std::env::var_os("MOADIM_TMUX_BIN");
unsafe {
std::env::set_var("MOADIM_TMUX_BIN", "/tmp/moadim-test-tmux-override");
}
assert_eq!(super::session::tmux_bin(), "/tmp/moadim-test-tmux-override");
unsafe {
match previous {
Some(value) => std::env::set_var("MOADIM_TMUX_BIN", value),
None => std::env::remove_var("MOADIM_TMUX_BIN"),
}
}
}
#[test]
fn parse_workbench_name_splits_slug_and_timestamp() {
assert_eq!(parse_workbench_name("foo-123"), Some(("foo", 123)));
assert_eq!(
parse_workbench_name("my-routine-1700000000"),
Some(("my-routine", 1_700_000_000))
);
}
#[test]
fn parse_workbench_name_rejects_non_workbench_dirs() {
assert_eq!(parse_workbench_name("noseparator"), None);
assert_eq!(parse_workbench_name("foo-bar"), None); assert_eq!(parse_workbench_name("foo-"), None); assert_eq!(parse_workbench_name("-123"), None); }
#[test]
fn is_expired_compares_age_against_ttl() {
assert!(is_expired(1000, 0, 500)); assert!(!is_expired(1000, 600, 500)); assert!(!is_expired(1000, 1000, 0)); assert!(!is_expired(1000, 2000, 0));
}
fn touch_dir(parent: &std::path::Path, name: &str) {
std::fs::create_dir_all(parent.join(name)).unwrap();
}
#[test]
fn reap_dir_removes_only_finished_and_expired() {
let base = std::env::temp_dir().join("moadim-cleanup-reap-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "expired-100"); touch_dir(&base, "fresh-900"); touch_dir(&base, "running-100"); touch_dir(&base, "notawb"); std::fs::write(base.join("stray-50"), b"x").unwrap();
let now = 1000;
let ttl_for = |_slug: &str| 500u64; let alive = |session: &str| session == "moadim-running-100";
let removed = reap_dir(
&base,
now,
&ttl_for,
&never_expires_runtime,
&alive,
&noop_kill,
);
assert_eq!(removed, 1);
assert!(!base.join("expired-100").exists());
assert!(base.join("fresh-900").exists());
assert!(base.join("running-100").exists());
assert!(base.join("notawb").exists());
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn reap_dir_uses_per_slug_ttl() {
let base = std::env::temp_dir().join("moadim-cleanup-perslug-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "short-500");
touch_dir(&base, "long-500");
let now = 1000;
let ttl_for = |slug: &str| if slug == "short" { 100 } else { 100_000 };
let dead = |_session: &str| false;
let removed = reap_dir(
&base,
now,
&ttl_for,
&never_expires_runtime,
&dead,
&noop_kill,
);
assert_eq!(removed, 1);
assert!(!base.join("short-500").exists());
assert!(base.join("long-500").exists());
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn reap_dir_kills_hung_session_over_max_runtime_then_reaps() {
let base = std::env::temp_dir().join("moadim-cleanup-watchdog-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "hung-100");
let now = 1000;
let ttl_for = |_slug: &str| 500u64; let max_runtime_for = |_slug: &str| 300u64; let alive = |_session: &str| true; let killed = std::cell::RefCell::new(Vec::new());
let kill = |session: &str| killed.borrow_mut().push(session.to_string());
let removed = reap_dir(&base, now, &ttl_for, &max_runtime_for, &alive, &kill);
assert_eq!(removed, 1, "hung-then-killed workbench is reaped");
assert_eq!(killed.into_inner(), vec!["moadim-hung-100".to_string()]);
assert!(!base.join("hung-100").exists());
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn reap_dir_records_forced_kill_in_agent_log_when_ttl_not_yet_elapsed() {
let base = std::env::temp_dir().join("moadim-cleanup-watchdog-log-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "hung-900");
let now = 1000;
let ttl_for = |_slug: &str| 100_000u64; let max_runtime_for = |_slug: &str| 50u64; let alive = |_session: &str| true;
let killed = std::cell::RefCell::new(Vec::new());
let kill = |session: &str| killed.borrow_mut().push(session.to_string());
let removed = reap_dir(&base, now, &ttl_for, &max_runtime_for, &alive, &kill);
assert_eq!(
removed, 0,
"killed but TTL not elapsed -> left for a later sweep"
);
assert_eq!(killed.into_inner(), vec!["moadim-hung-900".to_string()]);
let log = std::fs::read_to_string(base.join("hung-900").join("agent.log")).unwrap();
assert!(log.contains("exceeded max runtime"));
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn reap_dir_does_not_kill_dead_session_missing_tmux() {
let base = std::env::temp_dir().join("moadim-cleanup-watchdog-dead-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "gone-100");
let now = 1000;
let ttl_for = |_slug: &str| 100u64;
let max_runtime_for = |_slug: &str| 100u64;
let dead = |_session: &str| false;
let killed = std::cell::RefCell::new(Vec::new());
let kill = |session: &str| killed.borrow_mut().push(session.to_string());
let removed = reap_dir(&base, now, &ttl_for, &max_runtime_for, &dead, &kill);
assert_eq!(removed, 1);
assert!(
killed.into_inner().is_empty(),
"no kill for an already-dead session"
);
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn watchdog_dir_kills_hung_session_without_reaping() {
let base = std::env::temp_dir().join("moadim-watchdog-kill-test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "hung-100"); touch_dir(&base, "fresh-900"); touch_dir(&base, "gone-100"); touch_dir(&base, "notawb"); std::fs::write(base.join("stray-50"), b"x").unwrap();
let now = 1000;
let max_runtime_for = |_slug: &str| 300u64; let alive = |session: &str| session != "moadim-gone-100";
let killed = std::cell::RefCell::new(Vec::new());
let kill = |session: &str| killed.borrow_mut().push(session.to_string());
let count = watchdog_dir(&base, now, &max_runtime_for, &alive, &kill);
assert_eq!(count, 1, "only the hung session is killed");
assert_eq!(killed.into_inner(), vec!["moadim-hung-100".to_string()]);
assert!(base.join("hung-100").exists());
assert!(base.join("fresh-900").exists());
assert!(base.join("gone-100").exists());
let log = std::fs::read_to_string(base.join("hung-100").join("agent.log")).unwrap();
assert!(log.contains("exceeded max runtime"));
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn watchdog_dir_returns_zero_when_dir_unreadable() {
let missing =
std::env::temp_dir().join(format!("moadim-watchdog-missing-{}", uuid::Uuid::new_v4()));
assert!(!missing.exists());
let max_runtime_for = |_slug: &str| 0u64;
let alive = |_session: &str| true;
assert_eq!(
watchdog_dir(&missing, 1000, &max_runtime_for, &alive, &noop_kill),
0
);
}
#[test]
fn kill_hung_sessions_scans_real_workbenches_dir() {
let home = std::env::temp_dir().join(format!("moadim-watchdog-{}", uuid::Uuid::new_v4()));
let previous = std::env::var_os("MOADIM_HOME_OVERRIDE");
unsafe {
std::env::set_var("MOADIM_HOME_OVERRIDE", &home);
}
let workbenches = crate::paths::workbenches_dir();
std::fs::create_dir_all(&workbenches).unwrap();
std::fs::create_dir_all(workbenches.join("orphan-1")).unwrap();
let store = super::super::model::new_store();
let killed = kill_hung_sessions(&store);
assert_eq!(killed, 0);
assert!(workbenches.join("orphan-1").exists());
unsafe {
match previous {
Some(value) => std::env::set_var("MOADIM_HOME_OVERRIDE", value),
None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
}
}
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn reap_dir_returns_zero_when_dir_unreadable() {
let missing =
std::env::temp_dir().join(format!("moadim-cleanup-missing-{}", uuid::Uuid::new_v4()));
assert!(!missing.exists());
let ttl_for = |_slug: &str| 0u64;
let dead = |_session: &str| false;
assert_eq!(
reap_dir(
&missing,
1000,
&ttl_for,
&never_expires_runtime,
&dead,
&noop_kill
),
0
);
}
#[cfg(unix)]
#[test]
fn reap_dir_counts_zero_when_remove_fails() {
use std::os::unix::fs::PermissionsExt as _;
let base = std::env::temp_dir().join(format!(
"moadim-cleanup-removefail-{}",
uuid::Uuid::new_v4()
));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
touch_dir(&base, "expired-100");
std::fs::write(base.join("expired-100").join("inner"), b"x").unwrap();
let mut perms = std::fs::metadata(&base).unwrap().permissions();
perms.set_mode(0o555);
std::fs::set_permissions(&base, perms).unwrap();
let now = 1000;
let ttl_for = |_slug: &str| 500u64; let dead = |_session: &str| false;
let removed = reap_dir(
&base,
now,
&ttl_for,
&never_expires_runtime,
&dead,
&noop_kill,
);
if base.join("expired-100").exists() {
assert_eq!(removed, 0);
}
let mut perms = std::fs::metadata(&base).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&base, perms).unwrap();
std::fs::remove_dir_all(&base).unwrap();
}
#[test]
fn cleanup_expired_workbenches_scans_real_workbenches_dir() {
let home = std::env::temp_dir().join(format!("moadim-cleanup-{}", uuid::Uuid::new_v4()));
let previous = std::env::var_os("MOADIM_HOME_OVERRIDE");
unsafe {
std::env::set_var("MOADIM_HOME_OVERRIDE", &home);
}
let workbenches = crate::paths::workbenches_dir();
std::fs::create_dir_all(&workbenches).unwrap();
std::fs::create_dir_all(workbenches.join("orphan-1")).unwrap();
let fresh_ts = now_secs();
std::fs::create_dir_all(workbenches.join(format!("recent-{fresh_ts}"))).unwrap();
std::fs::create_dir_all(workbenches.join("notawb")).unwrap();
let store = super::super::model::new_store();
let removed = cleanup_expired_workbenches(&store);
assert!(removed >= 1, "expected at least the orphan to be reaped");
assert!(!workbenches.join("orphan-1").exists());
assert!(workbenches.join(format!("recent-{fresh_ts}")).exists());
assert!(workbenches.join("notawb").exists());
unsafe {
match previous {
Some(value) => std::env::set_var("MOADIM_HOME_OVERRIDE", value),
None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
}
}
let _ = std::fs::remove_dir_all(&home);
}
#[cfg(unix)]
#[test]
fn cleanup_expired_workbenches_kills_a_live_hung_session() {
use std::os::unix::fs::PermissionsExt as _;
let home = std::env::temp_dir().join(format!("moadim-cleanup-hung-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&home).unwrap();
let stub_tmux = home.join("stub-tmux");
std::fs::write(&stub_tmux, b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&stub_tmux, std::fs::Permissions::from_mode(0o755)).unwrap();
let prev_home = std::env::var_os("MOADIM_HOME_OVERRIDE");
let prev_tmux = std::env::var_os("MOADIM_TMUX_BIN");
unsafe {
std::env::set_var("MOADIM_HOME_OVERRIDE", &home);
std::env::set_var("MOADIM_TMUX_BIN", &stub_tmux);
}
let workbenches = crate::paths::workbenches_dir();
std::fs::create_dir_all(&workbenches).unwrap();
std::fs::create_dir_all(workbenches.join("hung-1")).unwrap();
let store = super::super::model::new_store();
let removed = cleanup_expired_workbenches(&store);
assert_eq!(
removed, 1,
"the live-but-overrun workbench is killed then reaped"
);
assert!(!workbenches.join("hung-1").exists());
unsafe {
match prev_home {
Some(value) => std::env::set_var("MOADIM_HOME_OVERRIDE", value),
None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
}
match prev_tmux {
Some(value) => std::env::set_var("MOADIM_TMUX_BIN", value),
None => std::env::remove_var("MOADIM_TMUX_BIN"),
}
}
let _ = std::fs::remove_dir_all(&home);
}
fn routine_with(schedule: &str, ttl_secs: Option<u64>) -> super::super::model::Routine {
super::super::model::Routine {
id: "x".into(),
schedule: schedule.into(),
title: "t".into(),
agent: "claude".into(),
prompt: "p".into(),
repositories: vec![],
machines: vec![crate::machine::current_machine()],
enabled: true,
source: "managed".into(),
created_at: 0,
updated_at: 0,
last_manual_trigger_at: None,
last_scheduled_trigger_at: None,
ttl_secs,
max_runtime_secs: None,
}
}
#[test]
fn effective_ttl_caps_at_max_for_long_intervals() {
assert_eq!(
routine_with("@daily", None).effective_ttl_secs(),
MAX_TTL_SECS
);
}
#[test]
fn effective_ttl_follows_sub_hour_cron_interval() {
assert_eq!(
routine_with("*/10 * * * *", None).effective_ttl_secs(),
10 * 60
);
}
#[test]
fn effective_ttl_explicit_only_lowers() {
assert_eq!(routine_with("@daily", Some(42)).effective_ttl_secs(), 42);
assert_eq!(
routine_with("@daily", Some(u64::MAX)).effective_ttl_secs(),
MAX_TTL_SECS
);
assert_eq!(
routine_with("*/10 * * * *", Some(u64::MAX)).effective_ttl_secs(),
10 * 60
);
}
#[test]
fn effective_ttl_falls_back_to_cap_for_unparseable_schedule() {
assert_eq!(
routine_with("@reboot", None).effective_ttl_secs(),
MAX_TTL_SECS
);
}
#[test]
fn effective_max_runtime_defaults_to_cap_when_unset() {
assert_eq!(
routine_with("@daily", None).effective_max_runtime_secs(),
MAX_RUNTIME_SECS
);
}
#[test]
fn effective_max_runtime_follows_sub_hour_cron_interval() {
let mut routine = routine_with("*/10 * * * *", None);
assert_eq!(routine.effective_max_runtime_secs(), 10 * 60);
routine.max_runtime_secs = Some(u64::MAX);
assert_eq!(routine.effective_max_runtime_secs(), 10 * 60);
}
#[test]
fn effective_max_runtime_uses_explicit_value() {
let mut routine = routine_with("@daily", None);
routine.max_runtime_secs = Some(1234);
assert_eq!(routine.effective_max_runtime_secs(), 1234);
routine.max_runtime_secs = Some(u64::MAX);
assert_eq!(routine.effective_max_runtime_secs(), MAX_RUNTIME_SECS);
}
#[test]
fn effective_ttl_falls_back_to_cap_when_schedule_never_fires() {
assert_eq!(
routine_with("0 0 30 2 *", None).effective_ttl_secs(),
MAX_TTL_SECS
);
assert_eq!(
routine_with("0 0 30 2 *", Some(15)).effective_ttl_secs(),
15
);
}
#[test]
fn parse_workbench_name_overflowing_timestamp_returns_none() {
assert!(parse_workbench_name("slug-99999999999999999999").is_none());
}
#[test]
fn cron_interval_secs_returns_none_when_second_fire_not_found() {
assert!(super::ttl::cron_interval_secs("0 0 0 1 1 * 4999").is_none());
}