#![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 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 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);
}
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![],
enabled: true,
source: "managed".into(),
created_at: 0,
updated_at: 0,
last_triggered_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
);
}