use super::*;
#[test]
fn guard_runtime_state_default_is_empty() {
let state = crate::GuardRuntimeState::default();
assert!(state.heavy_since.is_empty());
assert!(state.notify_cooldowns.is_empty());
assert!(state.last_disk_state.is_empty());
assert!(state.oom_known_descendants.is_empty());
assert!(state.oom_pending_descendants.is_empty());
}
#[test]
fn oom_descendant_candidates_only_select_new_untracked_user_processes() {
let sample = |pid: i32, ppid: i32, starttime: u64, command: &str| ProcSample {
swap_mb: Some(0),
pid,
ppid,
cpu_percent: 0.0,
rss_mb: 1,
nice: 0,
command: command.to_string(),
args: String::new(),
starttime,
};
let samples = vec![
sample(10, 1, 10, "root"),
sample(11, 10, 11, "existing-child"),
sample(12, 10, 12, "new-child"),
sample(13, 12, 13, "new-grandchild"),
sample(14, 1, 14, "unrelated"),
sample(15, 10, 15, "kworker/0:1"),
sample(16, 10, 16, "editor"),
sample(17, 10, 17, "tracked-child"),
];
let known = HashSet::from([(11, 11)]);
let tracked = HashSet::from([10, 17]);
let exempt = HashSet::from(["editor".to_string()]);
let descendants = crate::process_descendant_samples(&samples, 10);
assert_eq!(
descendants
.iter()
.map(|sample| sample.pid)
.collect::<Vec<_>>(),
vec![11, 12, 13, 15, 16, 17]
);
let candidates = crate::oom_descendant_candidates(&samples, 10, &known, &tracked, &exempt);
assert_eq!(
candidates
.iter()
.map(|sample| sample.pid)
.collect::<Vec<_>>(),
vec![12, 13]
);
}
#[test]
fn runtime_adjustment_plan_includes_every_reversible_limiter() {
let mut state = crate::GuardRuntimeState::default();
state.reniced_pids.insert(
101,
crate::LegacyReniceState {
original_nice: 0,
applied_nice: 5,
identity: crate::ProcessIdentity {
comm: "legacy-worker".to_string(),
starttime: 1,
},
},
);
state.memory_reniced_pids.insert(
102,
crate::MemoryReniceState {
original_nice: 3,
applied_nice: 10,
identity: crate::ProcessIdentity {
comm: "memory-worker".to_string(),
starttime: 2,
},
},
);
state.oom_biased_pids.insert(
103,
(
-100,
crate::ProcessIdentity {
comm: "oom-worker".to_string(),
starttime: 3,
},
),
);
state.capped_pids.insert(
104,
(
"dracon-cap.service".to_string(),
"user.slice".to_string(),
crate::ProcessIdentity {
comm: "cap-worker".to_string(),
starttime: 4,
},
),
);
let plan = crate::runtime_adjustment_plan(&state);
assert_eq!(plan.len(), 4);
assert!(plan.contains(&crate::RuntimeAdjustment::Nice {
pid: 101,
original_nice: 0,
identity: crate::ProcessIdentity {
comm: "legacy-worker".to_string(),
starttime: 1,
},
scope: crate::NiceRestoreScope::Legacy,
}));
assert!(plan.contains(&crate::RuntimeAdjustment::Nice {
pid: 102,
original_nice: 3,
identity: crate::ProcessIdentity {
comm: "memory-worker".to_string(),
starttime: 2,
},
scope: crate::NiceRestoreScope::Memory,
}));
assert!(plan.contains(&crate::RuntimeAdjustment::OomBias {
pid: 103,
orig_adj: -100,
identity: crate::ProcessIdentity {
comm: "oom-worker".to_string(),
starttime: 3,
},
}));
assert!(plan.contains(&crate::RuntimeAdjustment::CpuCap {
pid: 104,
scope: "dracon-cap.service".to_string(),
orig_cgroup: "user.slice".to_string(),
identity: crate::ProcessIdentity {
comm: "cap-worker".to_string(),
starttime: 4,
},
}));
}
#[test]
fn guard_runtime_state_insert_and_retrieve_heavy_process() {
let mut state = crate::GuardRuntimeState::default();
state.heavy_since.insert(1234, (Instant::now(), 0));
assert!(state.heavy_since.contains_key(&1234));
state.heavy_since.remove(&1234);
assert!(!state.heavy_since.contains_key(&1234));
}
#[test]
fn guard_policy_disk_thresholds_are_public() {
let guard = GuardPolicy::default();
assert_eq!(guard.disk_warn_percent, 80);
assert_eq!(guard.disk_action_percent, 90);
assert_eq!(guard.disk_critical_percent, 95);
assert_eq!(guard.disk_early_warn_percent, 70);
}
#[test]
fn guard_report_can_be_created_with_alerts() {
use crate::GuardProcessAlert;
use crate::GuardReport;
let report = GuardReport {
enabled: true,
disk_use_percent: 72,
disk_state: "warn".to_string(),
sync_frozen: false,
extra_mounts: Vec::new(),
relocate_candidates: Vec::new(),
relocated_count: 0,
relocated_bytes: 0,
alerts: vec![GuardProcessAlert {
pid: 12345,
ppid: 1,
command: "cargo".to_string(),
args: "build".to_string(),
cpu_percent: 250.0,
rss_mb: 1024,
sustained_secs: 35,
action: "reniced".to_string(),
nice_value: 5,
}],
memory: None,
service_memory_growth: crate::service_growth::Tracker::default().observe(
&GuardPolicy::default(),
Vec::new(),
Instant::now(),
),
zombies: Vec::new(),
reap_candidates: Vec::new(),
reaped: Vec::new(),
disk_fill_gbph: None,
swap_issues: Vec::new(),
};
assert!(report.enabled);
assert_eq!(report.disk_use_percent, 72);
assert_eq!(report.alerts.len(), 1);
assert_eq!(report.alerts[0].pid, 12345);
}
#[test]
fn graduated_nice_value_cpu_tier_180_percent() {
assert_eq!(crate::graduated_nice_value(180.0, 0, 5, 0), 5);
assert_eq!(crate::graduated_nice_value(200.0, 0, 5, 0), 5);
}
#[test]
fn graduated_nice_value_cpu_tier_300_percent() {
assert_eq!(crate::graduated_nice_value(300.0, 0, 5, 0), 10);
assert_eq!(crate::graduated_nice_value(350.0, 0, 5, 0), 10);
}
#[test]
fn graduated_nice_value_cpu_tier_500_percent() {
assert_eq!(crate::graduated_nice_value(500.0, 0, 5, 0), 15);
assert_eq!(crate::graduated_nice_value(600.0, 0, 5, 0), 15);
}
#[test]
fn graduated_nice_value_memory_4gb() {
assert_eq!(crate::graduated_nice_value(0.0, 4096, 5, 0), 5);
}
#[test]
fn graduated_nice_value_memory_8gb() {
assert_eq!(crate::graduated_nice_value(0.0, 8192, 10, 0), 10);
}
#[test]
fn graduated_nice_value_below_all_tiers_uses_base() {
assert_eq!(crate::graduated_nice_value(50.0, 100, 3, 0), 3);
assert_eq!(crate::graduated_nice_value(100.0, 500, 7, 0), 7);
}
#[test]
fn graduated_nice_value_preserves_current_nice() {
assert_eq!(crate::graduated_nice_value(180.0, 0, 5, 10), 10);
assert_eq!(crate::graduated_nice_value(50.0, 100, 5, 10), 10);
}
#[test]
fn graduated_nice_value_negative_base_clamped_to_zero() {
assert_eq!(crate::graduated_nice_value(0.0, 0, -5, 0), 0);
}
#[test]
fn graduated_nice_value_high_base_clamped_to_max() {
assert_eq!(crate::graduated_nice_value(0.0, 0, 20, 0), 19);
}
#[test]
fn memory_offender_nice_value_uses_swap_aware_footprint() {
let mut p = crate::parse_ps_output("4242 1 0 102400 0 worker\n")
.pop()
.unwrap();
assert_eq!(p.rss_mb, 100);
p.swap_mb = Some(30 * 1024);
assert_eq!(crate::memory_pressure_footprint_mb(&p), 100 + 30 * 1024);
assert_eq!(crate::memory_offender_nice_value(&p, 0), 10);
p.swap_mb = None;
assert_eq!(crate::memory_offender_nice_value(&p, 0), 0);
}
#[test]
fn load_process_cmdline_skips_args_for_dead_pid() {
let mut p = crate::parse_ps_output("2147483647 1 0 102400 0 worker\n")
.pop()
.unwrap();
p.args = String::from("sentinel");
let out = crate::load_process_cmdline(p);
assert_eq!(out.args, "sentinel");
}
#[test]
fn parse_ps_output_extracts_all_fields() {
let output = "12345 1 50.0 1024 3 cargo\n 23456 12345 25.0 2048 7 rustc";
let samples = crate::parse_ps_output(output);
assert_eq!(samples.len(), 2);
assert_eq!(samples[0].pid, 12345);
assert_eq!(samples[0].ppid, 1);
assert_eq!(samples[0].cpu_percent, 50.0);
assert_eq!(samples[0].rss_mb, 1024 / 1024); assert_eq!(samples[0].nice, 3);
assert_eq!(samples[0].command, "cargo");
assert_eq!(samples[1].pid, 23456);
assert_eq!(samples[1].ppid, 12345);
assert_eq!(samples[1].cpu_percent, 25.0);
assert_eq!(samples[1].rss_mb, 2048 / 1024); assert_eq!(samples[1].nice, 7);
assert_eq!(samples[1].command, "rustc");
}
#[test]
fn parse_ps_output_empty_input() {
let samples = crate::parse_ps_output("");
assert!(samples.is_empty());
}
#[test]
fn parse_ps_output_malformed_lines_skipped() {
let output = "not_valid\n12345 1 75.0 512 0 cargo";
let samples = crate::parse_ps_output(output);
assert_eq!(samples.len(), 1);
assert_eq!(samples[0].pid, 12345);
assert_eq!(samples[0].cpu_percent, 75.0);
}
#[test]
fn nice_restore_privilege_requires_root_or_cap_sys_nice() {
let unprivileged = "Uid:\t1000\t1000\t1000\t1000\nCapEff:\t0000000000000000\n";
let privileged = "Uid:\t1000\t1000\t1000\t1000\nCapEff:\t0000000000800000\n";
let root = "Uid:\t0\t0\t0\t0\nCapEff:\t0000000000000000\n";
assert!(!crate::has_nice_restore_privilege_from_status(unprivileged));
assert!(crate::has_nice_restore_privilege_from_status(privileged));
assert!(crate::has_nice_restore_privilege_from_status(root));
}
#[test]
fn disk_state_ok_below_warn() {
let guard = GuardPolicy::default();
assert_eq!(crate::disk_state(50, &guard), "ok");
assert_eq!(crate::disk_state(79, &guard), "ok");
}
#[test]
fn disk_state_warn_between_warn_and_action() {
let guard = GuardPolicy::default();
assert_eq!(crate::disk_state(80, &guard), "warn");
assert_eq!(crate::disk_state(89, &guard), "warn");
}
#[test]
fn disk_state_action_between_action_and_critical() {
let guard = GuardPolicy::default();
assert_eq!(crate::disk_state(90, &guard), "action");
assert_eq!(crate::disk_state(94, &guard), "action");
}
#[test]
fn disk_state_critical_at_or_above_critical() {
let guard = GuardPolicy::default();
assert_eq!(crate::disk_state(95, &guard), "critical");
assert_eq!(crate::disk_state(100, &guard), "critical");
}
#[test]
fn parse_df_use_percent_works() {
let output = "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 19512345 15678901 3823444 80% /";
assert_eq!(crate::parse_df_use_percent(output), Some(80));
}
#[test]
fn parse_df_use_percent_parses_without_percent_sign() {
let output = "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 19512345 15678901 3823444 80 /";
assert_eq!(crate::parse_df_use_percent(output), Some(80));
}
#[test]
fn parse_df_use_percent_no_matching_line() {
assert_eq!(crate::parse_df_use_percent(""), None);
assert_eq!(crate::parse_df_use_percent("no df output here"), None);
}
#[test]
fn should_notify_first_time_allowed() {
let mut state = crate::GuardRuntimeState::default();
let result = crate::should_notify(&mut state, "test-key", 300);
assert!(result);
}
#[test]
fn should_notify_respects_cooldown() {
let mut state = crate::GuardRuntimeState::default();
let first = crate::should_notify(&mut state, "my-event", 300);
assert!(first);
let second = crate::should_notify(&mut state, "my-event", 300);
assert!(!second);
state.notify_cooldowns.remove("my-event");
let third = crate::should_notify(&mut state, "my-event", 300);
assert!(third);
}
#[test]
fn report_state_transition_suppresses_unchanged_events() {
let mut state = crate::GuardRuntimeState::default();
let (previous, first) = crate::report_state_transition(&mut state, "memory", "warn", 1800);
assert!(previous.is_none());
assert!(first);
let (previous, repeated) = crate::report_state_transition(&mut state, "memory", "warn", 1800);
assert_eq!(previous.as_deref(), Some("warn"));
assert!(!repeated, "unchanged state must not emit every guard cycle");
let (previous, recovery) = crate::report_state_transition(&mut state, "memory", "ok", 1800);
assert_eq!(previous.as_deref(), Some("warn"));
assert!(recovery, "state transitions should emit immediately");
}
#[test]
fn memory_pressure_flipping_between_warn_and_critical_is_not_a_transition() {
let mut state = crate::GuardRuntimeState::default();
let decision = crate::memory_pressure_report_decision(&mut state, "ok", 1800);
assert!(!decision.emit_event, "a calm pass must not emit");
assert!(!decision.notify, "a calm pass must not notify");
let mut events = 0usize;
let mut notifications = 0usize;
for index in 0..40 {
let level = if index % 2 == 0 { "warn" } else { "critical" };
let decision = crate::memory_pressure_report_decision(&mut state, level, 1800);
if decision.emit_event {
events += 1;
}
if decision.notify {
notifications += 1;
}
}
assert_eq!(
events, 1,
"an oscillating level must produce exactly one event (the arrival), got {events}"
);
assert_eq!(
notifications, 1,
"an oscillating level must produce exactly one notification (the arrival), got {notifications}"
);
}
#[test]
fn memory_pressure_arrival_still_emits_and_notifies() {
let mut state = crate::GuardRuntimeState::default();
assert!(!crate::memory_pressure_report_decision(&mut state, "ok", 1800).emit_event);
let arrival = crate::memory_pressure_report_decision(&mut state, "critical", 1800);
assert!(arrival.emit_event, "arrival into pressure must emit");
assert!(arrival.notify, "arrival into pressure must notify");
let sustained = crate::memory_pressure_report_decision(&mut state, "warn", 1800);
assert!(!sustained.emit_event, "a sustained condition must not re-emit");
assert!(!sustained.notify, "a sustained condition must not re-notify");
}
#[test]
fn memory_pressure_recovery_still_emits() {
let mut state = crate::GuardRuntimeState::default();
crate::memory_pressure_report_decision(&mut state, "warn", 1800);
let recovery = crate::memory_pressure_report_decision(&mut state, "ok", 1800);
assert!(recovery.emit_event, "recovery must emit");
assert!(!recovery.notify, "recovery must not notify: nothing is wrong");
}
#[test]
fn memory_pressure_note_explains_an_exempt_only_offender_set() {
let exempt = vec![("pi".to_string(), 3_072u64), ("chrome".to_string(), 4_608)];
let note = crate::build_mitigation_note(&[], &exempt);
assert!(
note.contains("no mitigation applied"),
"the note must say no mitigation was applied: {note}"
);
assert!(
note.contains("exempt"),
"the note must say the consumers are exempt: {note}"
);
assert!(note.contains("pi"), "the note must name the consumer: {note}");
assert!(
note.contains("chrome"),
"the note must name every named consumer: {note}"
);
}
#[test]
fn memory_pressure_note_reports_mitigation_candidates() {
let sample = ProcSample {
swap_mb: Some(64),
pid: 4242,
ppid: 1,
cpu_percent: 0.0,
rss_mb: 512,
nice: 0,
command: "node".to_string(),
args: String::new(),
starttime: 99,
};
let note = crate::build_mitigation_note(&[sample], &[]);
assert!(
!note.contains("no mitigation applied"),
"a report with candidates must not claim nothing was done: {note}"
);
assert!(
note.contains("mitigation candidates"),
"the note must point at the candidates: {note}"
);
}
#[test]
fn exempt_consumer_footprint_groups_by_command() {
let sample = |pid: i32, command: &str, rss_mb: u64| ProcSample {
swap_mb: Some(0),
pid,
ppid: 1,
cpu_percent: 0.0,
rss_mb,
nice: 0,
command: command.to_string(),
args: String::new(),
starttime: pid as u64,
};
let samples = vec![
sample(10, "pi", 300),
sample(11, "pi", 200),
sample(12, "chrome", 400),
sample(13, "node", 900),
];
let exempt: std::collections::HashSet<String> =
["pi".to_string(), "chrome".to_string()].into_iter().collect();
let rows = crate::exempt_consumer_footprint(&samples, &exempt);
assert_eq!(rows.len(), 2, "one row per exempt command: {rows:?}");
assert_eq!(rows[0].0, "pi", "the biggest consumer is named first");
assert_eq!(rows[1].0, "chrome");
assert_eq!(
rows[0].1, 500,
"the footprint of a command is summed across its processes"
);
assert_eq!(rows[1].1, 400);
assert!(
!rows.iter().any(|(command, _)| command == "node"),
"a non-exempt process must not be reported as an exempt consumer"
);
}
#[test]
fn memory_pressure_class_collapses_warn_and_critical() {
assert_eq!(crate::memory_pressure_class("ok"), "ok");
assert_eq!(crate::memory_pressure_class("warn"), "pressure");
assert_eq!(crate::memory_pressure_class("critical"), "pressure");
}
#[test]
fn memory_pressure_event_text_explains_an_exempt_only_offender_set() {
let exempt = vec![("pi".to_string(), 2_752u64), ("chrome".to_string(), 4_704)];
let note = crate::build_mitigation_note(&[], &exempt);
let message = crate::memory_pressure_event_message(
"critical",
"critical",
12,
29,
Some(2.65),
&[],
0,
¬e,
);
assert!(
message.contains("no mitigation applied"),
"the event text must say no mitigation was applied: {message}"
);
assert!(
message.contains("exempt"),
"the event text must say the consumers are exempt: {message}"
);
assert!(
message.contains("pi") && message.contains("chrome"),
"the event text must name the exempt consumers: {message}"
);
assert!(message.contains("critical"), "{message}");
assert!(message.contains("mem available 12%"), "{message}");
}
#[test]
fn memory_pressure_notification_text_explains_the_mitigation_outcome() {
let exempt = vec![("pi".to_string(), 2_752u64)];
let note = crate::build_mitigation_note(&[], &exempt);
let body = crate::memory_pressure_notification_body(
"critical",
12,
29,
Some(2.65),
"(none)",
0,
¬e,
);
assert!(
body.contains("no mitigation applied"),
"the notification body must say no mitigation was applied: {body}"
);
assert!(body.contains("pi"), "the notification body must name it: {body}");
}
#[tokio::test]
async fn swap_devices_check_records_its_state_every_pass() {
let guard = crate::GuardPolicy::default();
let mut state = crate::GuardRuntimeState::default();
let issues = crate::check_swap_devices(&guard, &mut state).await;
let recorded = state.report_states["swap-devices"].value.clone();
assert_eq!(
recorded,
if issues.is_empty() { "ok" } else { "wrong" },
"the recorded state must agree with what the pass found"
);
let again = crate::check_swap_devices(&guard, &mut state).await;
assert_eq!(again, issues);
assert_eq!(state.report_states["swap-devices"].value, recorded);
}
#[test]
fn memory_pressure_ignores_swap_occupancy_without_active_pressure() {
assert_eq!(
crate::classify_memory_pressure(false, true, false),
"ok",
"cold pages in swap are not active pressure"
);
assert_eq!(crate::classify_memory_pressure(true, false, false), "warn");
assert_eq!(
crate::classify_memory_pressure(true, true, false),
"critical"
);
assert_eq!(crate::classify_memory_pressure(false, false, true), "warn");
}
#[test]
fn swapin_velocity_flags_thrash_at_or_above_threshold() {
assert!(!crate::swapin_velocity_thrash(None, 1000));
assert!(!crate::swapin_velocity_thrash(Some(0.0), 1000));
assert!(!crate::swapin_velocity_thrash(Some(999.9), 1000));
assert!(crate::swapin_velocity_thrash(Some(1000.0), 1000));
assert!(crate::swapin_velocity_thrash(Some(170_000.0), 1000));
}
#[test]
fn swapin_velocity_threshold_defaults_to_the_legacy_fallback() {
assert_eq!(
crate::GuardPolicy::default().mem_swapin_warn_pages_per_sec,
1000
);
}
#[test]
fn memory_pressure_requires_persistence_before_transition() {
let mut state = crate::GuardRuntimeState::default();
let start = Instant::now();
let (stable, previous, changed) =
crate::stabilize_memory_pressure_at(&mut state, "warn", 120, start);
assert_eq!(stable, "ok");
assert!(previous.is_none());
assert!(!changed);
let (stable, _, changed) = crate::stabilize_memory_pressure_at(
&mut state,
"warn",
120,
start + Duration::from_secs(119),
);
assert_eq!(stable, "ok");
assert!(!changed);
let (stable, previous, changed) = crate::stabilize_memory_pressure_at(
&mut state,
"warn",
120,
start + Duration::from_secs(120),
);
assert_eq!(stable, "warn");
assert_eq!(previous.as_deref(), Some("ok"));
assert!(changed);
let (stable, _, changed) = crate::stabilize_memory_pressure_at(
&mut state,
"ok",
120,
start + Duration::from_secs(121),
);
assert_eq!(stable, "warn");
assert!(!changed);
let (stable, previous, changed) = crate::stabilize_memory_pressure_at(
&mut state,
"ok",
120,
start + Duration::from_secs(241),
);
assert_eq!(stable, "ok");
assert_eq!(previous.as_deref(), Some("warn"));
assert!(changed);
}
#[test]
fn a_slow_fill_warns_even_though_it_is_below_the_rapid_fill_threshold() {
use crate::{disk_trend_warning, DiskFillRate};
let gib = 1_073_741_824u64;
let warning = disk_trend_warning(DiskFillRate::Filling(10.0), 138 * gib, 24, 20.0)
.expect("a disk 13.8 hours from full must warn");
assert!(
(warning.hours - 13.8).abs() < 0.05,
"the figure must be the honest one: {}",
warning.hours
);
assert_eq!(warning.gbph, 10.0);
}
#[test]
fn the_rapid_fill_alarm_owns_the_fast_case() {
use crate::{disk_trend_warning, DiskFillRate};
let gib = 1_073_741_824u64;
assert!(
disk_trend_warning(DiskFillRate::Filling(55.8), 138 * gib, 24, 20.0).is_none(),
"a fast fill is the rapid-fill alarm's job — two alarms saying one thing is noise"
);
}
#[test]
fn an_unmeasured_rate_never_produces_a_deadline() {
use crate::{disk_trend_warning, DiskFillRate};
let gib = 1_073_741_824u64;
assert!(disk_trend_warning(DiskFillRate::Unmeasured, 138 * gib, 24, 20.0).is_none());
assert!(disk_trend_warning(DiskFillRate::NotFilling, 138 * gib, 24, 20.0).is_none());
}
#[test]
fn the_trend_horizon_bounds_the_warning() {
use crate::{disk_trend_warning, DiskFillRate};
let gib = 1_073_741_824u64;
assert!(disk_trend_warning(DiskFillRate::Filling(10.0), 138 * gib, 12, 20.0).is_none());
assert!(disk_trend_warning(DiskFillRate::Filling(10.0), 138 * gib, 0, 20.0).is_none());
}
#[test]
fn every_monitoring_capability_records_an_event() {
const SOURCE: &str = include_str!("main.rs");
const CHECKS: &[(&str, &str)] = &[
("fn check_disk_state_change(", "disk/state-change"),
("fn check_disk_early_warning(", "disk/early-warning"),
("fn check_rapid_disk_fill(", "disk/rapid-fill"),
("fn check_disk_trends(", "disk/trend-warning"),
("fn check_memory_pressure(", "memory/pressure"),
("fn check_heavy_processes(", "process/heavy"),
("fn check_zombie_processes(", "process/zombies"),
("fn check_inode_usage(", "filesystem/inode-warning"),
("fn check_large_logs(", "filesystem/log-size"),
("fn check_extra_mounts(", "disk/extra-mount"),
("fn check_reap_candidates(", "process/reap-candidates"),
("fn check_link_drift(", "links-drifted"),
];
for (signature, event_path) in CHECKS {
let body = top_level_fn_source(SOURCE, signature);
assert!(
body.contains("emit_event("),
"{} must record what it observed — a desktop popup is transient and is erased when the operator is away",
signature
);
assert!(
body.contains(event_path),
"{} must record it as `{}` so it can be found in the event stream",
signature,
event_path
);
}
}
#[test]
fn the_daemon_reports_the_secondary_mounts() {
const SOURCE: &str = include_str!("main.rs");
let pass = top_level_fn_source(SOURCE, "pub(crate) async fn run_guard_once(");
assert!(
pass.contains("disk_details_for(&mount)"),
"the guard pass must have been located — otherwise the assertion below would pass on an empty region"
);
assert!(
pass.contains("check_extra_mounts("),
"the guard pass must report the secondary mounts: the daemon discards the GuardReport, so a check outside the pass is invisible"
);
assert!(
!pass.contains("veprintln!(1, \"extra mount"),
"an unreadable secondary mount must be COLLECTED and reported, not dropped at verbosity 1"
);
}
#[test]
fn the_daemon_reports_the_abandoned_dev_server_worklist() {
const SOURCE: &str = include_str!("main.rs");
let pass = top_level_fn_source(SOURCE, "pub(crate) async fn run_guard_once(");
assert!(
pass.contains("reap_candidates(guard)"),
"the guard pass must have been located — otherwise the assertion below would pass on an empty region"
);
assert!(
pass.contains("check_reap_candidates("),
"the guard pass must report the abandoned dev/test worklist: the daemon discards the GuardReport, so a check outside the pass is invisible"
);
}
#[test]
fn auto_cleanup_result_default() {
let result = crate::AutoCleanupResult::default();
assert_eq!(result.cleaned_count, 0);
assert_eq!(result.reclaimed_bytes, 0);
assert!(result.cleaned_paths.is_empty());
assert!(result.protected_paths.is_empty());
}
#[test]
fn parse_meminfo_reads_all_four_fields() {
let sample = r#"MemTotal: 32754688 kB
MemFree: 1098776 kB
MemAvailable: 9010908 kB
SwapTotal: 32680964 kB
SwapFree: 11556016 kB
"#;
let m = crate::parse_meminfo(sample).expect("meminfo should parse");
assert_eq!(m.mem_total_kb, 32754688);
assert_eq!(m.mem_available_kb, 9010908);
assert_eq!(m.swap_total_kb, 32680964);
assert_eq!(m.swap_free_kb, 11556016);
assert_eq!(m.mem_available_percent(), 27);
assert_eq!(m.swap_used_percent(), 64);
}
#[test]
fn parse_meminfo_missing_swap_reports_zero_used() {
let sample = "MemTotal: 1048576 kB\nMemAvailable: 65536 kB\n";
let m = crate::parse_meminfo(sample).expect("meminfo should parse");
assert_eq!(m.mem_available_percent(), 6);
assert_eq!(m.swap_used_percent(), 0);
}
#[test]
fn parse_meminfo_garbage_returns_none() {
assert!(crate::parse_meminfo("not meminfo at all").is_none());
assert!(crate::parse_meminfo("").is_none());
}
#[test]
fn parse_pressure_memory_extracts_full_and_some() {
let psi = r#"some avg10=1.23 avg60=2.00 avg300=0.66 total=3743158654
full avg10=4.56 avg60=0.30 avg300=0.65 total=3579061268
"#;
let (full, some) = crate::parse_pressure_memory(psi).expect("psi should parse");
assert!((full - 4.56).abs() < 1e-9);
assert!((some - 1.23).abs() < 1e-9);
}
#[test]
fn parse_pressure_memory_no_full_line_returns_none() {
assert!(
crate::parse_pressure_memory("some avg10=1.00 avg60=1.00 avg300=1.00 total=1").is_none()
);
}
#[test]
fn parse_vmstat_swap_reads_counters() {
let vmstat = "pswpin 1022648496\npswpout 1187700919\nnr_free_pages 12345\n";
let (pin, pout) = crate::parse_vmstat_swap(vmstat).expect("vmstat should parse");
assert_eq!(pin, 1022648496);
assert_eq!(pout, 1187700919);
}
#[test]
fn record_swap_counters_preserves_pswpout() {
let mut state = crate::GuardRuntimeState::default();
crate::record_swap_counters(&mut state, 123, 456);
let (_, pswpin, pswpout) = state.prev_swap_counters.expect("counters recorded");
assert_eq!(pswpin, 123);
assert_eq!(pswpout, 456);
}
#[test]
fn parse_proc_stat_zombie_detects_z_state() {
let line = "1234 (chrome --type=renderer) Z 567 1 567 0 -1 4194304 42 0 0 0 0 0 0 0 20 0 1 0 999 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
let (pid, comm, ppid, starttime) =
crate::parse_proc_stat_zombie(line).expect("zombie stat should parse");
assert_eq!(pid, 1234);
assert_eq!(comm, "chrome --type=renderer");
assert_eq!(ppid, 567);
assert_eq!(starttime, 999);
}
#[test]
fn parse_proc_stat_zombie_ignores_non_zombies() {
let line = "42 (bash) S 1 42 42 0 -1 4194304 100 0 0 0 0 0 0 0 20 0 1 0 123 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
assert!(crate::parse_proc_stat_zombie(line).is_none());
}
#[test]
fn looks_credential_like_matches_known_patterns() {
assert!(crate::looks_credential_like("Login Data"));
assert!(crate::looks_credential_like("CREDENTIALS.md"));
assert!(crate::looks_credential_like("secrets.rs"));
assert!(crate::looks_credential_like(".env"));
assert!(crate::looks_credential_like("github.env"));
assert!(crate::looks_credential_like("id_ed25519.key"));
assert!(crate::looks_credential_like("backup.age"));
assert!(crate::looks_credential_like(".git-credentials"));
assert!(crate::looks_credential_like(".npmrc"));
assert!(crate::looks_credential_like("hosts.yml"));
assert!(crate::looks_credential_like("password-store"));
assert!(!crate::looks_credential_like("wallet-token-cache"));
}
#[test]
fn looks_credential_like_ignores_benign_names() {
assert!(!crate::looks_credential_like("target"));
assert!(!crate::looks_credential_like("node_modules"));
assert!(!crate::looks_credential_like("ai-vid-editor"));
assert!(!crate::looks_credential_like("nixpkgs"));
assert!(!crate::looks_credential_like("go"));
assert!(!crate::looks_credential_like("saved_stuff"));
assert!(!crate::looks_credential_like("references"));
}
#[test]
fn disk_fill_rate_computes_sustained_rate() {
let now = Instant::now();
let history: Vec<(Instant, u64)> = vec![
(now - Duration::from_secs(7200), 500u64 * 1024 * 1024 * 1024),
(now - Duration::from_secs(3600), 510u64 * 1024 * 1024 * 1024),
(now, 520u64 * 1024 * 1024 * 1024),
];
let rate = crate::disk_fill_rate(&history)
.rate_gbph()
.expect("rate should compute");
assert!((rate - 10.0).abs() < 0.5, "expected ~10 GiB/h, got {rate}");
}
#[test]
fn disk_fill_rate_requires_minimum_samples_and_span() {
let now = Instant::now();
assert_eq!(
crate::disk_fill_rate(&[]),
crate::DiskFillRate::Unmeasured,
"no samples at all is unmeasured, not 'not filling'"
);
assert_eq!(
crate::disk_fill_rate(&[(now, 100), (now, 200)]),
crate::DiskFillRate::Unmeasured,
"too few samples is unmeasured"
);
let short: Vec<(Instant, u64)> = vec![
(now - Duration::from_secs(10), 100),
(now - Duration::from_secs(5), 200),
(now, 300),
];
assert_eq!(
crate::disk_fill_rate(&short),
crate::DiskFillRate::Unmeasured,
"a window that spans under 60s is unmeasured"
);
}
#[test]
fn disk_fill_rate_reports_not_filling_when_the_disk_is_flat() {
let now = Instant::now();
let gib = 1024u64 * 1024 * 1024;
let history: Vec<(Instant, u64)> = vec![
(now - Duration::from_secs(7200), 600 * gib),
(now - Duration::from_secs(3600), 600 * gib),
(now, 600 * gib),
];
assert_eq!(
crate::disk_fill_rate(&history),
crate::DiskFillRate::NotFilling,
"a flat disk is genuinely not filling"
);
}
#[test]
fn a_shrinking_disk_is_unmeasured_because_a_decrease_starts_a_new_window() {
let now = Instant::now();
let gib = 1024u64 * 1024 * 1024;
let history: Vec<(Instant, u64)> = vec![
(now - Duration::from_secs(7200), 600 * gib),
(now - Duration::from_secs(3600), 550 * gib),
(now, 500 * gib),
];
assert_eq!(
crate::disk_fill_rate(&history),
crate::DiskFillRate::Unmeasured
);
}
#[test]
fn a_reclamation_makes_the_rate_unmeasured_rather_than_not_filling() {
let now = Instant::now();
let gib = 1024u64 * 1024 * 1024;
let history: Vec<(Instant, u64)> = vec![
(now - Duration::from_secs(600), 600 * gib),
(now - Duration::from_secs(300), 700 * gib),
(now - Duration::from_secs(10), 400 * gib),
(now, 410 * gib),
];
assert_eq!(
crate::disk_fill_rate(&history),
crate::DiskFillRate::Unmeasured,
"a fresh growth window that is too young to measure is unmeasured"
);
}
#[test]
fn oom_bias_target_raises_neutral_values_to_250() {
assert_eq!(crate::oom_bias_target(0), Some(250));
assert_eq!(crate::oom_bias_target(100), Some(250));
assert_eq!(crate::oom_bias_target(-100), Some(250));
assert_eq!(crate::oom_bias_target(-300), Some(250));
}
#[test]
fn oom_bias_target_never_raises_already_biased_or_protected() {
assert_eq!(crate::oom_bias_target(250), None);
assert_eq!(crate::oom_bias_target(500), None);
assert_eq!(crate::oom_bias_target(1000), None);
assert_eq!(crate::oom_bias_target(-500), None);
assert_eq!(crate::oom_bias_target(-800), None);
assert_eq!(crate::oom_bias_target(-1000), None);
}
#[test]
fn oom_bias_target_boundaries() {
assert_eq!(crate::oom_bias_target(249), Some(250));
assert_eq!(crate::oom_bias_target(250), None);
assert_eq!(crate::oom_bias_target(-499), Some(250));
assert_eq!(crate::oom_bias_target(-500), None);
}
#[test]
fn memory_limiter_policy_defaults_are_safe() {
let p = crate::GuardPolicy::default();
assert!(!p.freeze_sync_at_action, "sync freeze must be opt-in");
assert!(!p.auto_renice, "CPU priority changes must be opt-in");
assert!(p.auto_renice_on_memory, "renice-on-memory default on");
assert!(p.bias_oom_on_pressure, "oom-bias default on");
assert_eq!(p.memory_pressure_sustain_secs, 120);
assert_eq!(p.report_repeat_secs, 1800);
assert_eq!(
p.cap_offenders_cpu_percent, 0,
"CPUQuota offender caps default OFF (opt-in)"
);
}
#[test]
fn normalize_guard_policy_clamps_cpu_cap_percent() {
let mut policy = crate::GuardPolicy {
cap_offenders_cpu_percent: u32::MAX,
..Default::default()
};
crate::normalize_guard_policy(&mut policy);
assert_eq!(policy.cap_offenders_cpu_percent, 100);
policy.cap_offenders_cpu_percent = 100;
crate::normalize_guard_policy(&mut policy);
assert_eq!(policy.cap_offenders_cpu_percent, 100);
}
#[test]
fn resolve_bin_falls_back_to_bare_name_off_nixos() {
assert_eq!(
crate::resolve_bin("dracon-definitely-not-a-binary"),
"dracon-definitely-not-a-binary"
);
assert_eq!(
crate::resolve_bin("dracon-definitely-not-a-binary"),
"dracon-definitely-not-a-binary"
);
}
#[test]
fn resolve_bin_strict_refuses_unresolvable_names() {
let err = crate::resolve_bin_strict("dracon-definitely-not-a-binary").unwrap_err();
assert!(
format!("{err:#}").contains("refusing PATH-relative exec"),
"strict resolve must refuse, not fall back: {err:#}"
);
}
#[test]
fn reap_policy_from_guard_maps_thresholds_and_shares_exempts() {
let guard = crate::GuardPolicy {
reap_report_min_idle_hours: 48,
reap_report_max_cpu_seconds: 120,
reap_report_signatures: "vite,mytool".to_string(),
process_exempt_names: "keepme,alsokeep".to_string(),
..Default::default()
};
let policy = crate::reap_policy_from_guard(&guard);
assert_eq!(policy.min_idle_hours, 48);
assert_eq!(policy.max_cpu_seconds, 120);
assert_eq!(
policy.signatures,
vec!["mytool".to_string(), "vite".to_string()]
);
assert!(policy.exempt_names.contains(&"keepme".to_string()));
assert!(policy.exempt_names.contains(&"alsokeep".to_string()));
}
#[test]
fn reap_candidates_runs_against_live_proc_without_panicking() {
let guard = crate::GuardPolicy::default();
let found = crate::reap_candidates(&guard);
for c in &found {
assert!(c.idle_hours >= guard.reap_report_min_idle_hours);
assert!(c.cpu_seconds <= guard.reap_report_max_cpu_seconds);
assert!(!c.args.is_empty());
}
}
#[test]
fn zombie_since_keys_carry_starttime_and_prune() {
let mut state = crate::GuardRuntimeState::default();
state
.zombies_since
.insert((999_998, 12345), std::time::Instant::now());
let _ = crate::zombie_details(&mut state);
assert!(
!state.zombies_since.contains_key(&(999_998, 12345)),
"stale zombie key for a vanished pid must be pruned"
);
}
#[test]
fn unavailable_identity_defer_drops_at_retry_limit() {
let mut attempts = std::collections::HashMap::new();
assert!(!crate::record_unavailable_attempt(&mut attempts, 4242));
assert!(!crate::record_unavailable_attempt(&mut attempts, 4242));
assert!(crate::record_unavailable_attempt(&mut attempts, 4242));
assert_eq!(attempts.get(&4242), Some(&3));
assert!(!crate::record_unavailable_attempt(&mut attempts, 4243));
}
#[test]
fn clean_node_modules_flag_defaults_true_and_gates_toml_off() {
let policy: crate::GuardPolicy = toml::from_str("").expect("empty policy parses with defaults");
assert!(policy.clean_node_modules);
let policy: crate::GuardPolicy =
toml::from_str("clean_node_modules = false\n").expect("flag-off parses");
assert!(!policy.clean_node_modules);
}
#[test]
fn policy_load_roundtrip_memory_limiter_knobs() {
let policy: crate::GuardPolicy = toml::from_str("").expect("empty policy parses with defaults");
assert!(policy.auto_renice_on_memory);
assert!(policy.bias_oom_on_pressure);
assert_eq!(policy.cap_offenders_cpu_percent, 0);
let policy: crate::GuardPolicy = toml::from_str(
"auto_renice_on_memory = false\nbias_oom_on_pressure = false\ncap_offenders_cpu_percent = 50\n",
)
.expect("explicit knobs parse");
assert!(!policy.auto_renice_on_memory);
assert!(!policy.bias_oom_on_pressure);
assert_eq!(policy.cap_offenders_cpu_percent, 50);
}
fn runtime_with_pending_nice(pid: i32) -> GuardRuntimeState {
let mut state = GuardRuntimeState::default();
state.memory_reniced_pids.insert(
pid,
MemoryReniceState {
original_nice: 3,
applied_nice: 10,
identity: ProcessIdentity {
comm: "worker".to_string(),
starttime: 42,
},
},
);
state
}
#[test]
fn a_fully_restored_reload_applies_the_new_policy_and_clears_runtime() {
let mut guard = GuardPolicy::default();
let mut runtime = runtime_with_pending_nice(4242);
let new_policy = GuardPolicy {
disk_warn_percent: 55,
..Default::default()
};
let outcome = apply_policy_reload(&mut guard, &mut runtime, new_policy, true);
assert_eq!(outcome, ReloadOutcome::Applied);
assert_eq!(guard.disk_warn_percent, 55, "new policy must be live");
assert!(
runtime.memory_reniced_pids.is_empty(),
"a clean restore must reset the runtime so no old state survives"
);
}
#[test]
fn a_partial_restore_keeps_the_previous_policy_live() {
let mut guard = GuardPolicy {
disk_warn_percent: 80,
..Default::default()
};
let mut runtime = runtime_with_pending_nice(4242);
let new_policy = GuardPolicy {
disk_warn_percent: 55,
..Default::default()
};
let outcome = apply_policy_reload(&mut guard, &mut runtime, new_policy, false);
assert_eq!(outcome, ReloadOutcome::Deferred);
assert_eq!(
guard.disk_warn_percent, 80,
"a deferred reload must not adopt the new policy"
);
assert_eq!(
runtime.memory_reniced_pids.len(),
1,
"the pending adjustment must be retained so the still-armed mitigation retries it"
);
}
#[test]
fn a_partial_restore_cannot_strand_an_adjustment_the_new_policy_disables() {
let mut guard = GuardPolicy {
auto_renice_on_memory: true,
..Default::default()
};
let mut runtime = runtime_with_pending_nice(4242);
let new_policy = GuardPolicy {
auto_renice_on_memory: false,
..Default::default()
};
let outcome = apply_policy_reload(&mut guard, &mut runtime, new_policy, false);
assert_eq!(outcome, ReloadOutcome::Deferred);
assert!(
guard.auto_renice_on_memory,
"the mitigation that produced the outstanding adjustment must stay armed, \
otherwise the process is reniced forever with no path to restore it"
);
assert!(
runtime.memory_reniced_pids.contains_key(&4242),
"the outstanding adjustment must survive so it is retried"
);
}
#[test]
fn a_partial_restore_with_only_threshold_changes_is_also_deferred() {
let mut guard = GuardPolicy::default();
let mut runtime = runtime_with_pending_nice(7);
let new_policy = GuardPolicy {
interval_secs: 5,
..Default::default()
};
assert_eq!(
apply_policy_reload(&mut guard, &mut runtime, new_policy, false),
ReloadOutcome::Deferred
);
assert_ne!(
guard.interval_secs, 5,
"the new interval must not be adopted"
);
}
#[test]
fn disabling_the_guard_is_applied_even_after_a_partial_restore() {
let mut guard = GuardPolicy::default();
let mut runtime = runtime_with_pending_nice(99);
let new_policy = GuardPolicy {
enabled: false,
..Default::default()
};
let outcome = apply_policy_reload(&mut guard, &mut runtime, new_policy, false);
assert_eq!(outcome, ReloadOutcome::Applied);
assert!(!guard.enabled, "enabled = false must always take effect");
assert_eq!(
runtime.memory_reniced_pids.len(),
1,
"state is retained for the shutdown restore, not dropped"
);
}
#[test]
fn reload_is_idempotent_across_repeated_sighups() {
let mut guard = GuardPolicy::default();
let mut runtime = GuardRuntimeState::default();
for _ in 0..5 {
let p = GuardPolicy {
disk_warn_percent: 70,
..Default::default()
};
assert_eq!(
apply_policy_reload(&mut guard, &mut runtime, p, true),
ReloadOutcome::Applied
);
}
assert_eq!(guard.disk_warn_percent, 70);
assert!(runtime.memory_reniced_pids.is_empty());
}
#[test]
fn a_reload_that_succeeds_after_a_deferred_one_recovers() {
let mut guard = GuardPolicy::default();
let mut runtime = runtime_with_pending_nice(1);
let p1 = GuardPolicy {
disk_warn_percent: 55,
..Default::default()
};
assert_eq!(
apply_policy_reload(&mut guard, &mut runtime, p1, false),
ReloadOutcome::Deferred
);
let p2 = GuardPolicy {
disk_warn_percent: 55,
..Default::default()
};
assert_eq!(
apply_policy_reload(&mut guard, &mut runtime, p2, true),
ReloadOutcome::Applied
);
assert_eq!(guard.disk_warn_percent, 55);
assert!(runtime.memory_reniced_pids.is_empty());
}
fn gated_release_markers(src: &str) -> Result<Vec<String>, String> {
let start = src
.find("if pressure == \"ok\" {")
.ok_or_else(|| "the pressure-release block was not found".to_string())?;
let gate = src[start..]
.find("if can_restore_nice {")
.map(|i| start + i)
.ok_or_else(|| {
"the renice half is no longer gated on the restore capability".to_string()
})?;
let body = &src[gate..];
let mut depth = 0i32;
let mut end = None;
for (i, ch) in body.char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = Some(gate + i);
break;
}
}
_ => {}
}
}
let end = end.ok_or_else(|| "the capability gate is not brace-balanced".to_string())?;
let gated = &src[gate..end];
let mut wrongly_gated = Vec::new();
for marker in [
"Restore oom_score_adj after the release window",
"Lift CPUQuota scopes after the release window",
] {
if gated.contains(marker) {
wrongly_gated.push(marker.to_string());
}
if !src[start..].contains(marker) {
return Err(format!(
"'{marker}' no longer exists: the release was deleted, not just ungated"
));
}
}
if !gated.contains("mem-unrenice") {
return Err(
"the renice release left the can_restore_nice gate, but its apply is still gated"
.to_string(),
);
}
Ok(wrongly_gated)
}
#[test]
fn oom_and_cpu_releases_are_not_gated_by_the_nice_capability() {
let wrongly = gated_release_markers(include_str!("main.rs"))
.expect("the release block must be shaped as the fix requires");
assert!(
wrongly.is_empty(),
"these releases are inside the can_restore_nice gate again, so a host without \
CAP_SYS_NICE can never release them: {wrongly:?}"
);
}
#[test]
fn the_structural_guard_catches_a_reintroduced_defect() {
let pre_fix = r#"
if pressure == "ok" {
if can_restore_nice {
eprintln!("mem-unrenice");
}
// Restore oom_score_adj after the release window.
let x = 1;
// Lift CPUQuota scopes after the release window.
let y = 2;
} else {
}
"#;
let pre_fix_nested = pre_fix.replace(
" if can_restore_nice {\n eprintln!(\"mem-unrenice\");\n }\n",
" if can_restore_nice {\n eprintln!(\"mem-unrenice\");\n // Restore oom_score_adj after the release window.\n let x = 1;\n // Lift CPUQuota scopes after the release window.\n let y = 2;\n }\n",
);
let wrongly = gated_release_markers(&pre_fix_nested)
.expect("the synthetic snippet must still be parseable");
assert_eq!(
wrongly.len(),
2,
"the guard must flag both releases when they are nested in the gate: {wrongly:?}"
);
}
#[test]
fn pending_oom_descendant_retries_are_bounded() {
let mut pending = OomPendingDescendant {
root_pid: 1,
original_adj: 0,
identity: ProcessIdentity {
starttime: 2,
comm: "child".to_string(),
},
attempts: 0,
};
for attempt in 1..OOM_DESCENDANT_RETRY_LIMIT {
assert!(
!oom_descendant_attempts_exhausted(&mut pending),
"attempt {attempt} must still be retried"
);
assert_eq!(pending.attempts, attempt);
}
assert!(
oom_descendant_attempts_exhausted(&mut pending),
"attempt {} must give up so the root is not pinned forever",
OOM_DESCENDANT_RETRY_LIMIT
);
assert_eq!(pending.attempts, OOM_DESCENDANT_RETRY_LIMIT);
}
#[test]
fn remembered_oom_descendants_are_pruned_past_the_cap() {
use std::collections::HashSet;
let mut state = GuardRuntimeState::default();
let identity = ProcessIdentity {
starttime: 1,
comm: "root".to_string(),
};
state.oom_biased_pids.insert(100, (0, identity.clone()));
let live = (999, 42u64);
let mut known: HashSet<(i32, u64)> = HashSet::new();
known.insert(live);
for i in 0..OOM_KNOWN_DESCENDANTS_CAP {
known.insert((1000 + i as i32, u64::MAX - i as u64));
}
assert!(
known.len() > OOM_KNOWN_DESCENDANTS_CAP,
"the fixture must exceed the cap to exercise the prune"
);
state.oom_known_descendants.insert(100, known);
state
.oom_known_descendants
.insert(200, HashSet::from([(1, 1)]));
let all_processes = vec![crate::ProcSample {
swap_mb: Some(0),
pid: live.0,
ppid: 1,
cpu_percent: 0.0,
rss_mb: 0,
nice: 0,
command: "child".to_string(),
args: String::new(),
starttime: live.1,
}];
prune_oom_known_descendants(&mut state, &all_processes);
assert!(
!state.oom_known_descendants.contains_key(&200),
"a root that is no longer biased must lose its bookkeeping"
);
let kept = &state.oom_known_descendants[&100];
assert!(
kept.len() <= OOM_KNOWN_DESCENDANTS_CAP,
"the set must not stay above the cap, got {}",
kept.len()
);
assert!(
kept.contains(&live),
"a descendant that is still alive must be remembered"
);
}
#[test]
fn the_mitigation_ledger_survives_a_restart() {
let dir = std::env::temp_dir().join(format!(
"dracon-system-mitigation-ledger-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time")
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("guard-mitigations.json");
let me = std::process::id() as i32;
let live_identity = {
let stat = std::fs::read_to_string(format!("/proc/{me}/stat")).unwrap_or_default();
let after = stat.rsplit_once(") ").map(|(_, rest)| rest.to_string());
let starttime = after
.as_deref()
.and_then(|rest| rest.split_whitespace().nth(19))
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0);
ProcessIdentity {
starttime,
comm: "self".to_string(),
}
};
let mut before = GuardRuntimeState::default();
before.memory_reniced_pids.insert(
me,
MemoryReniceState {
original_nice: 0,
applied_nice: 5,
identity: live_identity.clone(),
},
);
before
.oom_biased_pids
.insert(me, (0, live_identity.clone()));
persist_mitigations_to(&mut before, &path);
assert!(path.exists(), "the ledger must be written");
let first = std::fs::metadata(&path).unwrap().modified().unwrap();
persist_mitigations_to(&mut before, &path);
let second = std::fs::metadata(&path).unwrap().modified().unwrap();
assert_eq!(
first, second,
"an unchanged pass must not rewrite the ledger"
);
let mut after_restart = GuardRuntimeState::default();
hydrate_mitigations_from(&path, &mut after_restart);
assert!(
after_restart.memory_reniced_pids.contains_key(&me),
"a renice left applied by the previous run must be adopted"
);
assert!(
after_restart.oom_biased_pids.contains_key(&me),
"an oom bias left applied by the previous run must be adopted"
);
let mut recycled = GuardRuntimeState::default();
let mut bad = std::fs::read_to_string(&path).unwrap();
bad = bad.replace(
&format!("\"starttime\":{}", live_identity.starttime),
"\"starttime\":999999999",
);
std::fs::write(&path, bad).unwrap();
hydrate_mitigations_from(&path, &mut recycled);
assert!(
recycled.memory_reniced_pids.is_empty() && recycled.oom_biased_pids.is_empty(),
"a ledger entry whose identity does not match must be ignored, not adopted"
);
std::fs::write(&path, "{ not json").unwrap();
let mut corrupt = GuardRuntimeState::default();
hydrate_mitigations_from(&path, &mut corrupt);
assert!(
corrupt.memory_reniced_pids.is_empty(),
"a corrupt ledger must not block the daemon"
);
let _ = std::fs::remove_dir_all(dir);
}
fn cap_cgroup_fixture(label: &str) -> (std::path::PathBuf, i32, ProcessIdentity) {
let dir = std::env::temp_dir().join(format!(
"dracon-system-{label}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time")
.as_nanos()
));
std::fs::create_dir_all(&dir).expect("scratch dir");
let me = std::process::id() as i32;
let stat = std::fs::read_to_string(format!("/proc/{me}/stat")).unwrap_or_default();
let starttime = stat
.rsplit_once(") ")
.and_then(|(_, rest)| rest.split_whitespace().nth(19))
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0);
let identity = ProcessIdentity {
starttime,
comm: "self".to_string(),
};
(dir.join("guard-mitigations.json"), me, identity)
}
#[test]
fn mitigation_ledger_round_trips_the_capped_pids_original_cgroup() {
let (path, me, identity) = cap_cgroup_fixture("ledger-cap-cgroup");
let orig_cgroup = "user.slice/user-1000.slice/user@1000.service/app.slice";
let mut before = GuardRuntimeState::default();
before.capped_pids.insert(
me,
(
"run-r1.service".to_string(),
orig_cgroup.to_string(),
identity.clone(),
),
);
persist_mitigations_to(&mut before, &path);
let mut after_restart = GuardRuntimeState::default();
hydrate_mitigations_from(&path, &mut after_restart);
let (scope, restored_cgroup, _) = after_restart
.capped_pids
.get(&me)
.expect("a cap left applied by the previous run must be adopted");
assert_eq!(scope, "run-r1.service");
assert_eq!(
restored_cgroup, orig_cgroup,
"the cgroup to restore to must survive the restart, not hydrate empty"
);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn a_legacy_ledger_without_the_cgroup_still_hydrates_and_never_guesses_a_path() {
let (path, me, identity) = cap_cgroup_fixture("ledger-cap-legacy");
let legacy = serde_json::json!({
"records": [{
"pid": me,
"original_nice": serde_json::Value::Null,
"original_oom_adj": serde_json::Value::Null,
"cap_scope": "run-r9.service",
"identity": {"starttime": identity.starttime, "comm": "self"},
}]
})
.to_string();
assert!(
!legacy.contains("cap_orig_cgroup"),
"the legacy fixture must not contain the newer field"
);
std::fs::write(&path, legacy).unwrap();
let mut after_restart = GuardRuntimeState::default();
hydrate_mitigations_from(&path, &mut after_restart);
let (_, restored_cgroup, _) = after_restart
.capped_pids
.get(&me)
.expect("a legacy ledger entry with a matching identity must still be adopted");
assert_eq!(
restored_cgroup, "",
"a legacy ledger has no cgroup, and must be adopted with it empty"
);
let current = "user.slice/user-1000.slice/user@1000.service/run-r9.service";
assert_eq!(
restore_cgroup_target("", current).as_deref(),
Some("user.slice/user-1000.slice/user@1000.service"),
"with no recorded cgroup the pid goes back to the scope unit's parent"
);
assert_eq!(
restore_cgroup_target("user.slice/app.slice", current).as_deref(),
Some("user.slice/app.slice")
);
assert_eq!(restore_cgroup_target("", "run-r9.service"), None);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn disk_fill_rate_recovers_after_large_reclamation() {
let now = Instant::now();
let gib = 1024 * 1024 * 1024;
let history = [
(now - Duration::from_secs(120), 800 * gib),
(now - Duration::from_secs(90), 801 * gib),
(now - Duration::from_secs(60), 600 * gib),
(now - Duration::from_secs(30), 601 * gib),
(now, 602 * gib),
];
let rate = crate::disk_fill_rate(&history).rate_gbph().unwrap();
assert!((rate - 120.0).abs() < 0.01);
assert_eq!(
crate::disk_fill_rate(&history[..3]),
crate::DiskFillRate::Unmeasured
);
assert_eq!(
crate::disk_fill_rate(&history[..4]),
crate::DiskFillRate::Unmeasured
);
}
#[test]
fn disk_fill_rate_ignores_a_single_burst_inside_the_window() {
let now = Instant::now();
let gib = 1024u64 * 1024 * 1024;
let step = 30u64;
let steady = 20.0 / 120.0;
let mut history: Vec<(Instant, u64)> = vec![
(now - Duration::from_secs(step * 29), 300 * gib),
(now - Duration::from_secs(step * 28), 301 * gib),
];
let mut used = 100.0f64;
for i in 0..28u64 {
used += steady;
if i == 26 || i == 27 {
used += 2.4;
}
history.push((
now - Duration::from_secs(step * (27 - i)),
(used * gib as f64) as u64,
));
}
assert!(history.windows(2).all(|pair| pair[0].0 < pair[1].0));
let rate = crate::disk_fill_rate(&history)
.rate_gbph()
.expect("the window has a measurable rate");
assert!(
(rate - 20.0).abs() < 2.0,
"a single burst must not become the published rate: expected ~20 GiB/h, got {rate}"
);
assert!(
rate < 45.0,
"the published rate ({rate} GiB/h) must not track the burst"
);
}
#[test]
fn the_published_rate_is_stable_when_a_burst_arrives() {
let now = Instant::now();
let gib = 1024u64 * 1024 * 1024;
let step = 30u64;
let steady = 20.0 / 120.0;
let mut history: Vec<(Instant, u64)> = Vec::new();
let mut used = 100.0f64;
for i in 0..30u64 {
used += steady;
history.push((
now - Duration::from_secs(step * (29 - i)),
(used * gib as f64) as u64,
));
}
let before = crate::disk_fill_rate(&history)
.rate_gbph()
.expect("a steady fill measures");
assert!(
(before - 20.0).abs() < 2.0,
"expected ~20 GiB/h, got {before}"
);
used += 4.8;
history.push((now, (used * gib as f64) as u64));
let after = crate::disk_fill_rate(&history)
.rate_gbph()
.expect("the window still measures");
assert!(
(after - before).abs() < 5.0,
"one burst moved the published rate from {before} to {after} GiB/h"
);
}
#[test]
fn rapid_fill_warning_reports_user_available_runway() {
let message = crate::rapid_disk_fill_message(40.0, 90, 20 * 1024 * 1024 * 1024);
assert!(message.contains("20.0 GiB available"));
assert!(message.contains("0.5 hours until full if growth continues"));
let full = crate::rapid_disk_fill_message(40.0, 100, 0);
assert!(full.contains("0.0 hours"));
}
#[tokio::test]
async fn disk_reclamation_rearms_the_rapid_fill_notification() {
let guard = crate::GuardPolicy::default();
let mut state = crate::GuardRuntimeState::default();
crate::report_state_transition(&mut state, "disk-rapid-fill", "rapid", 3600);
let now = Instant::now();
state.disk_bytes_history = vec![
(now - Duration::from_secs(60), 800),
(now - Duration::from_secs(30), 801),
];
assert!(
crate::check_rapid_disk_fill(&guard, &mut state, 600, 60, 400)
.await
.is_none()
);
assert_eq!(
state.report_states["disk-rapid-fill"].value, "rapid",
"an unmeasurable rate must HOLD the state, not re-arm it"
);
let threshold = guard.disk_rapid_fill_gbph;
let reclaimed = crate::disk_rapid_observed_state(
crate::DiskFillRate::NotFilling,
true,
threshold,
);
assert_eq!(
reclaimed.map(|(state, _)| state),
Some("ok"),
"a non-positive slope must state the ok verdict"
);
let clock = Instant::now();
crate::report_state_transition_stabilized(
&mut state,
"disk-rapid-fill",
"ok",
guard.report_repeat_secs,
crate::REPORT_STATE_SUSTAIN_SECS,
clock,
);
let settled = crate::report_state_transition_stabilized(
&mut state,
"disk-rapid-fill",
"ok",
guard.report_repeat_secs,
crate::REPORT_STATE_SUSTAIN_SECS,
clock + Duration::from_secs(crate::REPORT_STATE_SUSTAIN_SECS + 1),
);
assert!(settled.changed, "the reclamation is accepted after the sustain window");
let resume_at = clock + Duration::from_secs(2 * crate::REPORT_STATE_SUSTAIN_SECS + 2);
let resume_candidate = crate::report_state_transition_stabilized(
&mut state,
"disk-rapid-fill",
"rapid",
guard.report_repeat_secs,
crate::REPORT_STATE_SUSTAIN_SECS,
resume_at,
);
assert!(
!resume_candidate.changed,
"a re-entry must hold for the sustain window before it is accepted"
);
let resumed = crate::report_state_transition_stabilized(
&mut state,
"disk-rapid-fill",
"rapid",
guard.report_repeat_secs,
crate::REPORT_STATE_SUSTAIN_SECS,
resume_at + Duration::from_secs(crate::REPORT_STATE_SUSTAIN_SECS + 1),
);
assert!(
resumed.changed,
"resumed growth is a fresh transition and re-notifies"
);
}
#[tokio::test]
async fn the_pass_stores_the_measured_fill_rate_for_the_scratch_report() {
let guard = crate::GuardPolicy::default();
let mut state = crate::GuardRuntimeState::default();
let now = Instant::now();
let gib = 1024u64 * 1024 * 1024;
state.disk_bytes_history = vec![
(now - Duration::from_secs(7200), 500 * gib),
(now - Duration::from_secs(3600), 510 * gib),
];
let reported = crate::check_rapid_disk_fill(&guard, &mut state, 520 * gib, 50, 100 * gib).await;
let stored = state
.disk_fill_rate
.rate_gbph()
.expect("the pass must have measured a rate");
assert!(
(stored - 10.0).abs() < 0.5,
"expected ~10 GiB/h, got {stored}"
);
assert_eq!(
reported,
Some(stored),
"the alarm and the stored rate must be the same figure"
);
}
#[test]
fn the_scratch_eta_does_not_claim_to_be_the_hours_until_the_disk_is_full() {
let gib = 1024u64 * 1024 * 1024;
let text = crate::scratch_eta_text(16 * gib, crate::DiskFillRate::Filling(51.3));
assert!(
!text.contains("hours until full"),
"the scratch figure must not reuse the alarm's wording: {text}"
);
assert!(
text.contains("0.3 hours of writes at 51.3 GiB/h"),
"the figure must say what it measures: {text}"
);
let not_filling = crate::scratch_eta_text(16 * gib, crate::DiskFillRate::NotFilling);
assert!(
not_filling.contains("the disk is not filling"),
"a flat disk says so: {not_filling}"
);
let unmeasured = crate::scratch_eta_text(16 * gib, crate::DiskFillRate::Unmeasured);
assert!(
unmeasured.contains("cannot be measured yet"),
"an unmeasurable rate says so: {unmeasured}"
);
assert!(
!unmeasured.contains("the disk is not filling"),
"an unmeasurable rate must NOT claim the disk is not filling: {unmeasured}"
);
}
#[tokio::test]
async fn an_unmeasurable_rate_is_stored_as_unmeasured_not_as_not_filling() {
let guard = crate::GuardPolicy::default();
let mut state = crate::GuardRuntimeState::default();
assert_eq!(state.disk_fill_rate, crate::DiskFillRate::Unmeasured);
assert!(
crate::check_rapid_disk_fill(&guard, &mut state, 600, 60, 400)
.await
.is_none()
);
assert_eq!(
state.disk_fill_rate,
crate::DiskFillRate::Unmeasured,
"no history yet is unmeasured — it is NOT 'the disk is not filling'"
);
}
#[test]
fn the_namespace_link_parses_to_an_inode_number() {
assert_eq!(
parse_namespace_inode(Path::new("user:[4026531837]")),
Some(4026531837)
);
assert_eq!(
parse_namespace_inode(Path::new("user:[4026532749]")),
Some(4026532749)
);
assert_eq!(
parse_namespace_inode(Path::new("user:[not-a-number]")),
None
);
assert_eq!(parse_namespace_inode(Path::new("pid:[4026531837]")), None);
assert_eq!(parse_namespace_inode(Path::new("4026531837")), None);
}
#[test]
fn a_capability_in_one_namespace_does_not_reach_another() {
assert!(nice_restore_namespace_ok(
Some(4026531837),
Some(4026531837)
));
assert!(!nice_restore_namespace_ok(
Some(4026532749),
Some(4026531837)
));
assert!(!nice_restore_namespace_ok(
Some(4026531837),
Some(4026532749)
));
assert!(!nice_restore_namespace_ok(None, Some(4026531837)));
assert!(!nice_restore_namespace_ok(Some(4026531837), None));
assert!(!nice_restore_namespace_ok(None, None));
}
#[test]
fn a_permission_failure_is_permanent_but_a_transient_one_is_not() {
assert!(nice_restore_failure_is_permanent(
"renice exited with status exit status: 1: renice: failed to set priority for 42 (process ID): Permission denied"
));
assert!(nice_restore_failure_is_permanent("Operation not permitted"));
assert!(!nice_restore_failure_is_permanent(
"renice exited with status exit status: 1"
));
assert!(!nice_restore_failure_is_permanent(
"failed to invoke /usr/bin/renice"
));
}
fn fake_proc_tree(name: &str, pid: i32, comm: &str, starttime: u64, ns: Option<u64>) -> PathBuf {
let root = std::env::temp_dir().join(format!("dracon-ns-{}-{name}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(root.join("self")).expect("self entry");
let dir = root.join(pid.to_string());
fs::create_dir_all(&dir).expect("pid dir");
fs::write(dir.join("comm"), format!("{comm}\n")).expect("comm");
let stat = format!(
"{pid} ({comm}) S 1 {pid} {pid} 0 0 -1 4194304 0 0 0 0 0 0 0 20 0 1 0 {starttime} 0 0 0 0 0 0 0 0 0 0 0 0 17 2 0 0 0 0 0"
);
fs::write(dir.join("stat"), stat).expect("stat");
if let Some(ns) = ns {
let ns_dir = dir.join("ns");
fs::create_dir_all(&ns_dir).expect("ns dir");
symlink(format!("user:[{ns}]"), ns_dir.join("user")).expect("ns symlink");
}
root
}
#[test]
fn the_user_namespace_is_read_from_proc() {
let root = fake_proc_tree("read", 4242, "minimax-code", 424_200, Some(4026531837));
assert_eq!(user_namespace_inode(&root, 4242), Some(4026531837));
assert_eq!(user_namespace_inode(&root, 9999), None);
let _ = fs::remove_dir_all(&root);
}
#[tokio::test]
async fn a_sighup_reload_is_not_deferred_by_an_unrestorable_nice_adjustment() {
let starttime = 424_200;
let own = crate::parse_namespace_inode(&fs::read_link("/proc/self/ns/user").expect("self ns"));
let other_ns = own.map(|ns| ns.wrapping_add(1)).unwrap_or(4026531837);
let root = fake_proc_tree("sighup", 4242, "minimax-code", starttime, Some(other_ns));
let mut state = crate::GuardRuntimeState::default();
state.reniced_pids.insert(
4242,
LegacyReniceState {
original_nice: 0,
applied_nice: 5,
identity: ProcessIdentity {
comm: "minimax-code".to_string(),
starttime,
},
},
);
let restored = crate::restore_runtime_adjustments_with_samples(
&mut state,
Path::new("/nonexistent/renice"),
Path::new("/nonexistent/systemctl"),
&root,
&[],
)
.await;
assert!(
restored,
"an unrestorable adjustment must not defer the policy reload"
);
assert!(
state.reniced_pids.is_empty(),
"the unrestorable adjustment must be dropped, not retried forever"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn the_credential_heuristic_only_flags_names_that_are_credential_files() {
assert!(!crate::looks_credential_like("android-chrome-192x192.png"));
assert!(!crate::looks_credential_like("chrome.CdV59toY.css"));
assert!(!crate::looks_credential_like(
"artifact-secret-scan-2026-10-01.test.ts"
));
assert!(!crate::looks_credential_like(
"aud-271-premium-token-validity-2026-08-05.test.ts"
));
assert!(!crate::looks_credential_like("02-borrowed-credentials.md"));
assert!(!crate::looks_credential_like("01-the-archivists-secret.md"));
assert!(!crate::looks_credential_like("trust_token.o"));
assert!(!crate::looks_credential_like("pmbtoken.o"));
assert!(crate::looks_credential_like("Login Data"));
assert!(crate::looks_credential_like("id_ed25519.key"));
assert!(crate::looks_credential_like("cli.key"));
assert!(crate::looks_credential_like(".env"));
assert!(crate::looks_credential_like(".env.production"));
assert!(crate::looks_credential_like("client_secret.json"));
assert!(crate::looks_credential_like("secrets.yaml"));
assert!(crate::looks_credential_like("credentials_backup"));
assert!(crate::looks_credential_like("id_ed25519"));
}
#[test]
fn a_disabled_rust_cleanup_is_reported_once_and_a_recovery_is_confirmed() {
use crate::proactive_rust_report;
assert_eq!(proactive_rust_report(None, false), Some("disabled"));
assert_eq!(proactive_rust_report(Some(false), false), None);
assert_eq!(proactive_rust_report(Some(false), true), Some("enabled"));
assert_eq!(proactive_rust_report(Some(true), true), None);
assert_eq!(proactive_rust_report(None, true), None);
assert_eq!(proactive_rust_report(Some(true), false), Some("disabled"));
}
pub(crate) fn top_level_fn_source(source: &str, signature: &str) -> String {
let start = source
.find(signature)
.unwrap_or_else(|| panic!("{signature} must exist"));
let rest = &source[start + signature.len()..];
let end = rest
.find("\nfn ")
.or_else(|| rest.find("\npub(crate) fn "))
.or_else(|| rest.find("\npub(crate) async fn "))
.or_else(|| rest.find("\nimpl "))
.unwrap_or(rest.len());
rest[..end].to_string()
}
#[test]
fn the_disabled_rust_cleanup_report_is_not_tied_to_a_cleanup_tier() {
const SOURCE: &str = include_str!("main.rs");
let call = "report_proactive_rust_state(guard.auto_cleanup_rust)";
assert_eq!(
SOURCE.matches(call).count(),
1,
"the rust-cleanup report must have exactly one call site — the guard pass — so no cleanup tier can skip it"
);
let pass = top_level_fn_source(SOURCE, "pub(crate) async fn run_guard_once(");
assert!(
pass.contains("disk_details_for"),
"the guard pass must have been located — otherwise the assertion below would pass on an empty region"
);
assert!(
pass.contains(call),
"the guard pass must report the rust-cleanup state, so the report appears in every cleanup tier"
);
let dispatch = top_level_fn_source(SOURCE, "fn spawn_background_cleanup(");
assert!(
dispatch.contains("CleanupMode::Proactive"),
"the tier dispatch must have been located — otherwise the assertion below would pass on an empty region"
);
assert!(
!dispatch.contains("report_proactive_rust_state("),
"the rust-cleanup report must not live in the cleanup tier dispatch: the action tier (85%) takes the pressure branch and skips the proactive one"
);
}
#[test]
fn an_env_template_is_not_a_credential_file() {
assert!(!crate::looks_credential_like(".env.example"));
assert!(!crate::looks_credential_like(".env.ovh.example"));
assert!(!crate::looks_credential_like(".env.sample"));
assert!(!crate::looks_credential_like(".env.template"));
assert!(!crate::looks_credential_like(".env.dist"));
assert!(crate::looks_credential_like(".env"));
assert!(crate::looks_credential_like(".env.local"));
assert!(crate::looks_credential_like(".env.production"));
}
#[test]
fn a_credential_word_in_the_middle_of_a_name_is_not_a_credential_file() {
assert!(crate::looks_credential_like("credentials_backup"));
assert!(!crate::looks_credential_like("02-borrowed-credentials"));
assert!(!crate::looks_credential_like("04-miras-secrets"));
assert!(!crate::looks_credential_like("wallet-token-cache"));
assert!(crate::looks_credential_like("token"));
}
#[test]
fn a_credential_word_that_leads_an_ordinary_compound_name_is_not_a_credential_file() {
assert!(!crate::looks_credential_like(
"secret_scan-a5b13cc6b5fee239.d"
));
assert!(!crate::looks_credential_like("secret_scan"));
assert!(!crate::looks_credential_like("token_bucket.rs"));
assert!(!crate::looks_credential_like("secrets-scan.rs"));
assert!(crate::looks_credential_like("credentials_backup"));
assert!(crate::looks_credential_like("password-store-backup"));
assert!(crate::looks_credential_like("client_secret.json"));
assert!(crate::looks_credential_like("secrets.yaml"));
assert!(crate::looks_credential_like("token.json"));
assert!(crate::looks_credential_like("password.txt"));
}
#[tokio::test]
async fn the_credential_guard_reports_the_disk_cost_of_what_it_keeps() {
let root = std::env::temp_dir().join(format!("dracon-kept-cost-{}", std::process::id()));
let files = root.join("files");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(files.join("big")).expect("big entry");
std::fs::create_dir_all(files.join("small")).expect("small entry");
std::fs::write(files.join("big").join("blob.bin"), vec![0u8; 4096]).expect("blob");
std::fs::write(files.join("small").join("blob.bin"), vec![0u8; 1024]).expect("blob");
let mut names = std::collections::HashSet::new();
names.insert("big".to_string());
names.insert("small".to_string());
let (total, sizes) = crate::kept_trash_entry_sizes(&files, &names).await;
assert_eq!(total, 5120, "the kept entries' size must be measured");
assert_eq!(sizes.len(), 2);
assert_eq!(sizes[0].0, "big", "the biggest kept entry must come first");
assert_eq!(sizes[0].1, 4096);
assert_eq!(sizes[1].1, 1024);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_oscillating_measurement_produces_no_transitions() {
let mut state = crate::GuardRuntimeState::default();
let start = Instant::now();
let mut events = 0usize;
let mut transitions = 0usize;
for pass in 0..40u64 {
let observed = if pass % 2 == 0 { "soon" } else { "ok" };
let transition = crate::report_state_transition_stabilized(
&mut state,
"disk-trend",
observed,
1800,
120,
start + Duration::from_secs(pass * 30),
);
if transition.changed {
transitions += 1;
}
if transition.emit_event {
events += 1;
}
}
assert_eq!(
transitions, 0,
"a value that never holds for the sustain window must never become the state"
);
assert_eq!(
events, 0,
"an oscillating measurement must produce no events at all"
);
}
#[test]
fn a_sustained_value_is_accepted_and_reported_once() {
let mut state = crate::GuardRuntimeState::default();
let start = Instant::now();
let mut events = 0usize;
let mut transitions = 0usize;
for pass in 0..60u64 {
let transition = crate::report_state_transition_stabilized(
&mut state,
"disk-trend",
"soon",
1800,
120,
start + Duration::from_secs(pass * 30),
);
if transition.changed {
transitions += 1;
}
if transition.emit_event {
events += 1;
}
}
assert_eq!(transitions, 1, "a sustained condition is one transition");
assert!(
events <= 3,
"a sustained condition must not produce an event per pass, got {events}"
);
}
#[test]
fn a_transition_is_reported_once_the_sustain_window_has_passed() {
let mut state = crate::GuardRuntimeState::default();
let start = Instant::now();
let first = crate::report_state_transition_stabilized(
&mut state,
"disk-trend",
"soon",
1800,
120,
start,
);
assert!(!first.changed, "the first observation is not yet a transition");
assert!(!first.emit_event, "the first observation emits nothing");
let inside = crate::report_state_transition_stabilized(
&mut state,
"disk-trend",
"soon",
1800,
120,
start + Duration::from_secs(60),
);
assert!(!inside.changed, "60 s in, the window has not passed");
assert!(!inside.emit_event, "60 s in, nothing is emitted");
let after = crate::report_state_transition_stabilized(
&mut state,
"disk-trend",
"soon",
1800,
120,
start + Duration::from_secs(130),
);
assert!(after.changed, "past the window, the transition is accepted");
assert!(after.emit_event, "past the window, the transition is reported");
}
#[test]
fn a_sustained_recovery_is_accepted_and_reported() {
let mut state = crate::GuardRuntimeState::default();
let start = Instant::now();
for pass in 0..6u64 {
crate::report_state_transition_stabilized(
&mut state,
"zombie-warning",
"over-threshold",
1800,
120,
start + Duration::from_secs(pass * 30),
);
}
let mut recoveries = 0usize;
for pass in 0..6u64 {
let transition = crate::report_state_transition_stabilized(
&mut state,
"zombie-warning",
"ok",
1800,
120,
start + Duration::from_secs(180 + pass * 30),
);
if transition.changed {
recoveries += 1;
}
}
assert_eq!(
recoveries, 1,
"a sustained recovery is one transition, not one per pass"
);
}
#[test]
fn the_stabilized_path_delays_the_first_report_but_loses_none() {
let mut plain = crate::GuardRuntimeState::default();
let mut stabilized = crate::GuardRuntimeState::default();
let start = Instant::now();
let mut plain_events = 0usize;
let mut stabilized_events = 0usize;
let mut first_stabilized_at: Option<u64> = None;
for pass in 0..240u64 {
let at = start + Duration::from_secs(pass * 30);
if crate::report_state_transition_at(&mut plain, "k", "warn", 1800, at).1 {
plain_events += 1;
}
let transition =
crate::report_state_transition_stabilized(&mut stabilized, "k", "warn", 1800, 120, at);
if transition.emit_event {
stabilized_events += 1;
first_stabilized_at.get_or_insert(pass * 30);
}
}
assert!(
(plain_events as i64 - stabilized_events as i64).abs() <= 1,
"stabilization must not lose or add reports over a long run: \
plain {plain_events}, stabilized {stabilized_events}"
);
assert!(
stabilized_events > 1,
"the sustained value must still be repeated on the configured interval"
);
let first = first_stabilized_at.expect("the sustained value must be reported");
assert!(
first <= 120,
"the first report must be delayed by at most the sustain window, got {first}s"
);
assert!(
first >= 120,
"the first report must wait for the window, got {first}s"
);
}
#[test]
fn a_rate_hovering_at_the_threshold_never_flips_state() {
let threshold = 20.0;
let mut rapid = crate::disk_rapid_verdict(threshold * 0.5, false, threshold);
assert!(!rapid, "below the entry threshold the disk is not rapid");
rapid = crate::disk_rapid_verdict(21.0, rapid, threshold);
assert!(rapid, "crossing the entry threshold from below enters rapid");
let mut flips = 0usize;
for rate in [19.0, 22.0, 18.0, 25.0, 17.0, 20.5, 19.5] {
let next = crate::disk_rapid_verdict(rate, rapid, threshold);
if next != rapid {
flips += 1;
}
rapid = next;
}
assert_eq!(
flips, 0,
"a rate wandering across the threshold must be absorbed by the deadband, got {flips} flips"
);
assert!(rapid, "having entered rapid, the verdict stays rapid inside the deadband");
}
#[test]
fn a_rate_falling_far_below_the_threshold_leaves_rapid() {
let threshold = 20.0;
let exit = threshold * crate::DISK_RAPID_FILL_EXIT_RATIO;
assert!(
!crate::disk_rapid_verdict(exit - 0.1, true, threshold),
"below the exit ratio the verdict must leave rapid"
);
assert!(
crate::disk_rapid_verdict(exit + 0.1, true, threshold),
"above the exit ratio the verdict must stay rapid"
);
assert!(!crate::disk_rapid_verdict(0.0, true, threshold));
}
#[test]
fn an_unmeasured_rate_holds_the_report_state_instead_of_rearming() {
let mut state = crate::GuardRuntimeState::default();
let start = Instant::now();
for pass in 0..5u64 {
crate::report_state_transition_stabilized(
&mut state,
"disk-rapid-fill",
"rapid",
1800,
120,
start + Duration::from_secs(pass * 30),
);
}
let held = state.report_states.get("disk-rapid-fill").unwrap().value.clone();
assert_eq!(held, "rapid");
let still_held = state.report_states.get("disk-rapid-fill").unwrap().value.clone();
assert_eq!(
still_held, "rapid",
"an unmeasurable rate must not reset the accepted state"
);
let after = crate::report_state_transition_stabilized(
&mut state,
"disk-rapid-fill",
"rapid",
1800,
120,
start + Duration::from_secs(600),
);
assert!(!after.changed, "no change, so no transition");
}
#[test]
fn memory_pressure_critical_to_ok_recovery_still_emits() {
let mut state = crate::GuardRuntimeState::default();
let arrival = crate::memory_pressure_report_decision(&mut state, "critical", 1800);
assert!(arrival.emit_event, "arrival into critical must emit");
assert!(arrival.notify, "arrival into critical must notify");
let recovery = crate::memory_pressure_report_decision(&mut state, "ok", 1800);
assert!(
recovery.emit_event,
"a literal critical -> ok recovery must emit a recovery event"
);
assert!(
!recovery.notify,
"recovery is not a new pressure arrival, so it must not raise an alarm"
);
}
#[test]
fn an_unavailable_service_growth_measurement_is_not_heartbeated() {
assert!(
crate::service_growth_should_emit(true, true, false, true),
"entry into unavailable must be reported once"
);
assert!(
!crate::service_growth_should_emit(true, true, false, false),
"an unchanged unavailable state must not repeat"
);
assert!(
crate::service_growth_should_emit(true, true, true, false),
"sustained growth must still repeat on the configured interval"
);
assert!(crate::service_growth_should_emit(true, false, false, true));
assert!(!crate::service_growth_should_emit(false, true, true, true));
}
#[test]
fn an_escalation_from_trend_to_rapid_is_one_report_not_two() {
let mut state = crate::GuardRuntimeState::default();
let repeat = 1800u64;
assert!(
crate::disk_rate_event_due(&state, repeat),
"the first report of a fill episode must be allowed"
);
state.last_disk_rate_event = Some(Instant::now());
assert!(
!crate::disk_rate_event_due(&state, repeat),
"an escalation 216 s after the trend warning must not commit a second \
event for the same fill episode, got a due event"
);
state.last_disk_rate_event = Some(Instant::now() - Duration::from_secs(repeat + 1));
assert!(
crate::disk_rate_event_due(&state, repeat),
"the fill-rate condition must be allowed to report again after the \
repeat interval, otherwise a worsening disk goes silent forever"
);
}