use super::*;
macro_rules! floor_knob_tests {
($($name:ident: $field:ident = $bad:expr => min $min:expr;)*) => {
$(
#[test]
fn $name() {
let mut p = GuardPolicy { $field: $bad, ..Default::default() };
let adjusted = normalize_guard_policy(&mut p);
assert!(
p.$field >= $min,
concat!(stringify!($field), " = {:?} is below its floor {}"),
$bad,
$min
);
assert!(
adjusted.contains(&stringify!($field)),
"{} must be reported as clamped",
stringify!($field)
);
}
)*
};
}
macro_rules! ceil_knob_tests {
($($name:ident: $field:ident = $bad:expr => max $max:expr;)*) => {
$(
#[test]
fn $name() {
let mut p = GuardPolicy { $field: $bad, ..Default::default() };
let adjusted = normalize_guard_policy(&mut p);
assert!(
p.$field <= $max,
concat!(stringify!($field), " = {:?} is above its ceiling {}"),
$bad,
$max
);
assert!(
adjusted.contains(&stringify!($field)),
"{} must be reported as clamped",
stringify!($field)
);
}
)*
};
}
macro_rules! band_knob_tests {
($($name:ident: $field:ident low $low:expr => $bad_lo:expr, high $high:expr => $bad_hi:expr;)*) => {
$(
#[test]
fn $name() {
let mut low = GuardPolicy { $field: $bad_lo, ..Default::default() };
let adjusted = normalize_guard_policy(&mut low);
assert!(
low.$field >= $low,
concat!(stringify!($field), " = {:?} is below its floor {}"),
$bad_lo,
$low
);
assert!(adjusted.contains(&stringify!($field)));
let mut high = GuardPolicy { $field: $bad_hi, ..Default::default() };
let adjusted = normalize_guard_policy(&mut high);
assert!(
high.$field <= $high,
concat!(stringify!($field), " = {:?} is above its ceiling {}"),
$bad_hi,
$high
);
assert!(adjusted.contains(&stringify!($field)));
}
)*
};
}
floor_knob_tests! {
interval_secs_floor: interval_secs = 0 => min 5;
auto_cleanup_interval_secs_floor: auto_cleanup_interval_secs = 0 => min 60;
report_repeat_secs_floor: report_repeat_secs = 0 => min 60;
proactive_cleanup_interval_cycles_floor: proactive_cleanup_interval_cycles = 0 => min 1;
process_rss_mb_floor: process_rss_mb = 0 => min 64;
process_sustain_secs_floor: process_sustain_secs = 0 => min 5;
release_after_secs_floor: release_after_secs = 0 => min 5;
notify_cooldown_secs_floor: notify_cooldown_secs = 0 => min 5;
memory_pressure_sustain_secs_floor: memory_pressure_sustain_secs = 0 => min 30;
guard_log_max_mb_floor: guard_log_max_mb = 0 => min 1;
log_max_truncate_mb_floor: log_max_truncate_mb = 0 => min 1;
cleanup_min_size_mb_floor: cleanup_min_size_mb = 0 => min 1;
rust_target_max_age_days_floor: rust_target_max_age_days = 0 => min 1;
relocate_min_size_mb_floor: relocate_min_size_mb = 0 => min 1;
relocate_max_moves_per_pass_floor: relocate_max_moves_per_pass = 0 => min 1;
}
#[test]
fn guard_log_max_mb_zero_would_silently_disable_rotation() {
let mut p = GuardPolicy {
guard_log_max_mb: 0,
..Default::default()
};
normalize_guard_policy(&mut p);
assert!(p.guard_log_max_mb >= 1);
}
ceil_knob_tests! {
cap_offenders_cpu_percent_ceiling: cap_offenders_cpu_percent = 500 => max 100;
log_preserve_header_lines_ceiling: log_preserve_header_lines = 50_000_000 => max LOG_PRESERVE_HEADER_LINES_MAX;
}
#[test]
fn renice_value_is_clamped_into_the_posix_nice_range() {
let mut low = GuardPolicy {
renice_value: -20,
..Default::default()
};
normalize_guard_policy(&mut low);
assert_eq!(low.renice_value, RENICE_VALUE_MIN);
let mut high = GuardPolicy {
renice_value: 40,
..Default::default()
};
normalize_guard_policy(&mut high);
assert_eq!(high.renice_value, RENICE_VALUE_MAX);
}
band_knob_tests! {
disk_warn_percent_band: disk_warn_percent low 1 => 0, high 100 => 200;
inode_warn_percent_band: inode_warn_percent low 1 => 0, high 100 => 200;
mem_available_warn_percent_band: mem_available_warn_percent low 1 => 0, high 100 => 200;
swap_used_warn_percent_band: swap_used_warn_percent low 1 => 0, high 200 => 250;
}
#[test]
fn float_knobs_scrub_nan_and_infinity() {
let mut p = GuardPolicy {
process_cpu_percent: f32::NAN,
mem_psi_full_warn: f64::NAN,
disk_rapid_fill_gbph: f64::NAN,
..Default::default()
};
let adjusted = normalize_guard_policy(&mut p);
assert!(
p.process_cpu_percent.is_finite(),
"NaN CPU threshold survived"
);
assert!(
p.mem_psi_full_warn.is_finite(),
"NaN PSI threshold survived"
);
assert!(p.disk_rapid_fill_gbph.is_finite(), "NaN fill-rate survived");
for field in [
"process_cpu_percent",
"mem_psi_full_warn",
"disk_rapid_fill_gbph",
] {
assert!(adjusted.contains(&field), "{field} must be reported");
}
}
#[test]
fn float_knobs_clamp_infinity() {
let mut p = GuardPolicy {
process_cpu_percent: f32::INFINITY,
mem_psi_full_warn: f64::INFINITY,
disk_rapid_fill_gbph: f64::INFINITY,
..Default::default()
};
normalize_guard_policy(&mut p);
assert!(p.process_cpu_percent.is_finite());
assert!(p.mem_psi_full_warn.is_finite());
assert!(p.disk_rapid_fill_gbph.is_finite());
}
#[test]
fn float_knobs_clamp_negative_infinity() {
let mut p = GuardPolicy {
process_cpu_percent: f32::NEG_INFINITY,
mem_psi_full_warn: f64::NEG_INFINITY,
disk_rapid_fill_gbph: f64::NEG_INFINITY,
..Default::default()
};
normalize_guard_policy(&mut p);
assert!(p.process_cpu_percent >= 1.0);
assert!(p.mem_psi_full_warn >= 0.0);
assert!(p.disk_rapid_fill_gbph >= 0.5);
}
#[test]
fn process_cpu_percent_ceiling_allows_multicore_readings() {
let mut p = GuardPolicy {
process_cpu_percent: 3200.0,
..Default::default()
};
let adjusted = normalize_guard_policy(&mut p);
assert_eq!(p.process_cpu_percent, 3200.0);
assert!(!adjusted.contains(&"process_cpu_percent"));
}
#[test]
fn disk_bands_are_ordered() {
let mut p = GuardPolicy {
disk_early_warn_percent: 99,
disk_warn_percent: 80,
disk_action_percent: 40,
disk_critical_percent: 10,
..Default::default()
};
normalize_guard_policy(&mut p);
assert!(p.disk_early_warn_percent <= p.disk_warn_percent);
assert!(p.disk_action_percent >= p.disk_warn_percent);
assert!(p.disk_critical_percent >= p.disk_action_percent);
assert!(p.disk_action_percent <= 100);
assert!(p.disk_critical_percent <= 100);
}
#[test]
fn pre_action_gates_stay_below_the_action_level() {
let mut p = GuardPolicy {
disk_action_percent: 90,
proactive_cleanup_percent: 95,
unfreeze_below_percent: 90,
..Default::default()
};
normalize_guard_policy(&mut p);
assert!(p.proactive_cleanup_percent < p.disk_action_percent);
assert!(p.unfreeze_below_percent < p.disk_action_percent);
}
#[test]
fn stuck_threshold_never_drops_below_sustain() {
let mut p = GuardPolicy {
process_sustain_secs: 300,
process_stuck_after_secs: 30,
..Default::default()
};
normalize_guard_policy(&mut p);
assert!(p.process_stuck_after_secs >= p.process_sustain_secs);
}
#[test]
fn sentinel_zero_knobs_are_never_clamped() {
let mut sentinel = GuardPolicy {
trend_warn_hours: 0,
zombie_threshold: 0,
log_size_mb: 0,
node_modules_max_age_days: 0,
tmp_min_age_hours: 0,
relocate_min_age_days: 0,
trash_min_age_days: 0,
rust_target_action_min_age_days: 0,
quarantine_ttl_days: 0,
cap_offenders_cpu_percent: 0,
reap_report_min_idle_hours: 0,
reap_report_max_cpu_seconds: 0,
nix_keep_generations: 0,
..Default::default()
};
let adjusted = normalize_guard_policy(&mut sentinel);
for knob in SENTINEL_ZERO_KNOBS {
assert!(
!adjusted.contains(knob),
"{knob} is a documented 0-sentinel and must not be clamped (adjusted: {adjusted:?})"
);
}
assert_eq!(sentinel.trend_warn_hours, 0);
assert_eq!(sentinel.quarantine_ttl_days, 0);
assert_eq!(sentinel.zombie_threshold, 0);
assert_eq!(sentinel.nix_keep_generations, 0);
assert_eq!(sentinel.rust_target_action_min_age_days, 0);
}
#[test]
fn sentinel_zero_knob_list_matches_the_policy_fields() {
let known = [
"trend_warn_hours",
"zombie_threshold",
"log_size_mb",
"node_modules_max_age_days",
"tmp_min_age_hours",
"relocate_min_age_days",
"trash_min_age_days",
"rust_target_action_min_age_days",
"quarantine_ttl_days",
"cap_offenders_cpu_percent",
"reap_report_min_idle_hours",
"reap_report_max_cpu_seconds",
"nix_keep_generations",
"quarantine_expire_interval_secs",
];
let mut listed: Vec<&&str> = SENTINEL_ZERO_KNOBS.iter().collect();
listed.sort();
let mut expected: Vec<&&str> = known.iter().collect();
expected.sort();
assert_eq!(listed, expected, "SENTINEL_ZERO_KNOBS drifted");
}
#[test]
fn blank_strings_fall_back_to_their_defaults() {
let mut p = GuardPolicy {
sync_freeze_marker: " ".to_string(),
quarantine_dir: String::new(),
notify_command: " ".to_string(),
..Default::default()
};
let adjusted = normalize_guard_policy(&mut p);
assert_eq!(p.sync_freeze_marker, default_sync_freeze_marker());
assert_eq!(p.quarantine_dir, default_quarantine_dir());
assert_eq!(p.notify_command, default_notify_command());
for field in ["sync_freeze_marker", "quarantine_dir", "notify_command"] {
assert!(adjusted.contains(&field), "{field} must be reported");
}
}
#[test]
fn storage_min_size_mb_floor() {
let mut s = StoragePolicy {
min_size_mb: 0,
..Default::default()
};
let adjusted = normalize_storage_policy(&mut s);
assert!(s.min_size_mb >= 1);
assert!(adjusted.contains(&"storage.min_size_mb"));
}
#[test]
fn system_policy_normalize_covers_both_sub_policies() {
let mut p = SystemPolicy {
storage: StoragePolicy {
min_size_mb: 0,
..Default::default()
},
guard: GuardPolicy {
interval_secs: 0,
guard_log_max_mb: 0,
..Default::default()
},
..Default::default()
};
p.normalize();
assert!(p.storage.min_size_mb >= 1);
assert!(p.guard.interval_secs >= 5);
assert!(p.guard.guard_log_max_mb >= 1);
}
#[test]
fn normalization_is_idempotent() {
let mut p = GuardPolicy {
disk_early_warn_percent: 200,
disk_warn_percent: 10,
disk_action_percent: 5,
disk_critical_percent: 1,
proactive_cleanup_percent: 200,
unfreeze_below_percent: 200,
interval_secs: 0,
renice_value: -99,
process_cpu_percent: f32::NAN,
guard_log_max_mb: 0,
log_preserve_header_lines: usize::MAX,
process_sustain_secs: 900,
process_stuck_after_secs: 1,
..Default::default()
};
let first = normalize_guard_policy(&mut p);
assert!(!first.is_empty(), "the fixture must actually be clamped");
let after_first = p.clone();
let second = normalize_guard_policy(&mut p);
assert!(
second.is_empty(),
"second pass re-clamped {second:?}; normalization is not idempotent"
);
assert!(
format!("{p:?}") == format!("{after_first:?}"),
"second pass mutated the policy"
);
}
#[test]
fn already_legal_policy_is_left_untouched() {
let mut p = GuardPolicy::default();
assert!(
normalize_guard_policy(&mut p).is_empty(),
"the shipped defaults must already satisfy every documented range"
);
}
#[test]
fn parsed_policy_is_normalized_at_the_load_boundary() {
let toml_src = r#"
[guard]
interval_secs = 0
guard_log_max_mb = 0
renice_value = -20
process_cpu_percent = nan
quarantine_ttl_days = 0
disk_warn_percent = 200
process_sustain_secs = 900
process_stuck_after_secs = 1
[storage]
min_size_mb = 0
"#;
let mut parsed: SystemPolicy = toml::from_str(toml_src).expect("fixture must parse");
parsed.normalize();
assert!(parsed.guard.interval_secs >= 5);
assert!(parsed.guard.guard_log_max_mb >= 1);
assert_eq!(parsed.guard.renice_value, 0);
assert!(parsed.guard.process_cpu_percent.is_finite());
assert_eq!(parsed.guard.quarantine_ttl_days, 0);
assert!(parsed.guard.disk_warn_percent <= 100);
assert!(parsed.guard.process_stuck_after_secs >= parsed.guard.process_sustain_secs);
assert!(parsed.storage.min_size_mb >= 1);
}
#[test]
fn parse_system_policy_is_the_normalizing_entry_point() {
let toml_src = r#"
[guard]
interval_secs = 0
guard_log_max_mb = 0
renice_value = -20
[storage]
min_size_mb = 0
"#;
let parsed = crate::parse_system_policy(toml_src, std::path::Path::new("fixture.toml"))
.expect("fixture must parse");
assert!(parsed.guard.interval_secs >= 5);
assert!(parsed.guard.guard_log_max_mb >= 1);
assert_eq!(parsed.guard.renice_value, 0);
assert!(parsed.storage.min_size_mb >= 1);
}
fn run_report_sequence(steps: &[&[&'static str]]) -> Vec<ClampReport> {
let mut state: Option<Vec<&'static str>> = None;
let mut decisions = Vec::with_capacity(steps.len());
for step in steps {
let decision = clamp_report_decision(state.as_deref(), step);
match &decision {
ClampReport::SilentNoState | ClampReport::Unchanged => {}
_ => state = Some(step.to_vec()),
}
decisions.push(decision);
}
decisions
}
const A: &[&str] = &["interval_secs"];
const B: &[&str] = &["disk_warn_percent", "interval_secs"];
const NONE: &[&str] = &[];
#[test]
fn first_out_of_range_policy_is_reported() {
assert_eq!(
run_report_sequence(&[A])[0],
ClampReport::Clamped(vec!["interval_secs"])
);
}
#[test]
fn an_in_range_policy_on_a_fresh_process_records_nothing() {
assert_eq!(
run_report_sequence(&[NONE])[0],
ClampReport::SilentNoState,
"an in-range policy must not report, and must not record state — \
otherwise the first real clamp looks like a transition from a known state"
);
}
#[test]
fn repeating_the_same_clamped_set_is_silent() {
let d = run_report_sequence(&[A, A, A, A]);
assert_eq!(d[0], ClampReport::Clamped(vec!["interval_secs"]));
for step in &d[1..] {
assert_eq!(*step, ClampReport::Unchanged, "a repeat must stay silent");
}
}
#[test]
fn returning_to_in_range_reports_the_recovery_once() {
let d = run_report_sequence(&[A, A, NONE, NONE]);
assert_eq!(d[0], ClampReport::Clamped(vec!["interval_secs"]));
assert_eq!(d[1], ClampReport::Unchanged);
assert_eq!(
d[2],
ClampReport::Recovered(vec!["interval_secs"]),
"the fix must be confirmed, not inferred from silence"
);
assert_eq!(d[3], ClampReport::Unchanged, "recovery must not repeat");
}
#[test]
fn a_changed_clamped_set_reports_again() {
let d = run_report_sequence(&[A, B]);
assert_eq!(d[0], ClampReport::Clamped(vec!["interval_secs"]));
assert_eq!(
d[1],
ClampReport::Clamped(vec!["disk_warn_percent", "interval_secs"])
);
}
#[test]
fn already_clean_stays_silent_across_many_passes() {
for d in run_report_sequence(&vec![NONE; 50]) {
assert_eq!(d, ClampReport::SilentNoState);
}
}
#[test]
fn repeated_passes_over_a_bad_policy_report_exactly_once() {
let d = run_report_sequence(&vec![B; 50]);
assert_eq!(
d[0],
ClampReport::Clamped(vec!["disk_warn_percent", "interval_secs"])
);
for step in &d[1..] {
assert_eq!(*step, ClampReport::Unchanged);
}
}
#[test]
fn shipped_example_template_is_already_legal() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("dracon-system.example.toml");
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let mut parsed: SystemPolicy =
toml::from_str(&content).unwrap_or_else(|e| panic!("example template must parse: {e}"));
let adjusted = normalize_guard_policy(&mut parsed.guard);
assert!(
adjusted.is_empty(),
"dracon-system.example.toml sets out-of-range values for {adjusted:?}; \
either fix the example or widen the range in normalize_guard_policy"
);
}
#[test]
fn plain_path_is_a_program_with_no_arguments() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send").expect("must parse");
assert_eq!(cmd.program, "/usr/bin/notify-send");
assert!(
cmd.args.is_empty(),
"backwards compatibility: no args expected"
);
}
#[test]
fn notify_send_urgency_flag_parses_into_two_arguments() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send -u critical").expect("must parse");
assert_eq!(cmd.program, "/usr/bin/notify-send");
assert_eq!(cmd.args, vec!["-u", "critical"]);
}
#[test]
fn multiple_flags_and_an_equals_style_value_parse() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send -u critical -i /tmp/icon.png -t 5s")
.expect("must parse");
assert_eq!(
cmd.args,
vec!["-u", "critical", "-i", "/tmp/icon.png", "-t", "5s"]
);
}
#[test]
fn runs_of_whitespace_separate_words() {
let cmd =
NotifyCommand::parse(" /usr/bin/notify-send \t -u critical ").expect("must parse");
assert_eq!(cmd.program, "/usr/bin/notify-send");
assert_eq!(cmd.args, vec!["-u", "critical"]);
}
#[test]
fn a_quoted_path_containing_spaces_stays_one_program() {
let split = NotifyCommand::parse("/opt/my notifier/bin/notify").expect("must parse");
assert_eq!(split.program, "/opt/my");
assert_eq!(split.args, vec!["notifier/bin/notify"]);
let quoted = NotifyCommand::parse("\"/opt/my notifier/bin/notify\"").expect("must parse");
assert_eq!(quoted.program, "/opt/my notifier/bin/notify");
assert!(quoted.args.is_empty());
}
#[test]
fn single_quotes_preserve_spaces() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send -t 'my title'").expect("must parse");
assert_eq!(cmd.args, vec!["-t", "my title"]);
}
#[test]
fn double_quotes_preserve_spaces() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send -t \"my title\"").expect("must parse");
assert_eq!(cmd.args, vec!["-t", "my title"]);
}
#[test]
fn quoted_empty_string_is_an_empty_argument_not_a_missing_one() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send -a '' -b \"\"").expect("must parse");
assert_eq!(cmd.args, vec!["-a", "", "-b", ""]);
}
#[test]
fn quotes_concatenate_with_adjacent_unquoted_text() {
let cmd =
NotifyCommand::parse("/usr/bin/notify-send -t pre'fix '\"-suf\"fix").expect("must parse");
assert_eq!(cmd.args, vec!["-t", "prefix -suffix"]);
}
#[test]
fn backslash_escapes_the_next_character() {
let cmd = NotifyCommand::parse("/usr/bin/notify-send -t my\\ title").expect("must parse");
assert_eq!(cmd.args, vec!["-t", "my title"]);
}
#[test]
fn backslash_inside_double_quotes_only_escapes_the_posix_set() {
let cmd = NotifyCommand::parse(r#"/usr/bin/notify-send -t "a\"b""#).expect("must parse");
assert_eq!(cmd.args, vec!["-t", "a\"b"]);
let cmd = NotifyCommand::parse(r#"/usr/bin/notify-send -t "a\nb""#).expect("must parse");
assert_eq!(cmd.args, vec!["-t", "a\\nb"]);
}
#[test]
fn single_quotes_are_literal_including_backslashes() {
let cmd = NotifyCommand::parse(r#"/usr/bin/notify-send -t 'a\b'"#).expect("must parse");
assert_eq!(cmd.args, vec!["-t", "a\\b"]);
}
#[test]
fn unterminated_quotes_are_an_error() {
for bad in [
"/usr/bin/notify-send -t 'oops",
"/usr/bin/notify-send -t \"oops",
"/usr/bin/notify-send -t oops\\",
] {
assert!(
NotifyCommand::parse(bad).is_err(),
"{bad} must be rejected rather than silently mis-parsed"
);
}
}
#[test]
fn shell_metacharacters_are_inert_arguments_not_syntax() {
let cmd = NotifyCommand::parse(
"/usr/bin/notify-send -t 'a; rm -rf /' -b '$(whoami)' -i `id` && echo pwned",
)
.expect("must parse");
assert_eq!(
cmd.args,
vec![
"-t",
"a; rm -rf /",
"-b",
"$(whoami)",
"-i",
"`id`",
"&&",
"echo",
"pwned",
]
);
assert_eq!(cmd.program, "/usr/bin/notify-send");
}
#[test]
fn a_relative_program_is_rejected() {
for bad in ["notify-send -u critical", "./notify-send", "sh -c ls"] {
let err = NotifyCommand::parse(bad).expect_err("must reject");
assert!(
err.contains("absolute path"),
"{bad} should be rejected for being relative, got: {err}"
);
}
}
#[test]
fn shell_and_privilege_programs_are_rejected() {
for bad in [
"/bin/sh -c 'curl evil.example'",
"/usr/bin/bash -c ls",
"/bin/dash",
"/usr/bin/zsh -c ls",
"/usr/bin/fish",
"/bin/busybox sh",
"/usr/bin/sudo /usr/bin/notify-send",
"/bin/su - root",
"/usr/bin/doas notify-send",
"/usr/bin/env FOO=1 /usr/bin/notify-send",
"/usr/bin/xargs notify-send",
"/usr/bin/systemd-run notify-send",
] {
let err = NotifyCommand::parse(bad).expect_err("must reject");
assert!(
err.contains("may not use"),
"{bad} must be refused as a shell/escalator, got: {err}"
);
}
}
#[test]
fn the_forbidden_list_matches_on_the_file_name_not_the_full_path() {
for path in [
"/bin/sh",
"/usr/bin/sh",
"/usr/local/bin/bash",
"/nix/store/x/bin/zsh",
] {
let err = NotifyCommand::parse(path).expect_err("must reject");
assert!(err.contains("may not use"), "{path} slipped through: {err}");
}
}
#[test]
fn a_real_notifier_is_not_on_the_forbidden_list() {
for good in [
"/usr/bin/notify-send -u critical",
"/run/current-system/sw/bin/notify-send",
"/opt/scripts/my-notifier.sh --channel ops",
] {
assert!(
NotifyCommand::parse(good).is_ok(),
"{good} is a legitimate notifier and must be accepted"
);
}
}
#[test]
fn an_empty_value_is_rejected() {
assert!(NotifyCommand::parse(" ").is_err());
}
#[test]
fn an_absent_log_dirs_resolves_to_the_default() {
let raw: Option<String> = None;
assert_eq!(
effective_log_dirs(&raw).as_deref(),
Some(default_log_dirs().as_str()),
"an unset key must resolve to the default, not to 'disabled'"
);
}
#[test]
fn an_explicitly_blank_log_dirs_stays_disabled() {
for blank in ["", " ", "\t"] {
let raw = Some(blank.to_string());
assert_eq!(
effective_log_dirs(&raw),
None,
"{blank:?} is the operator's explicit off-switch and must stay honoured"
);
}
}
#[test]
fn a_configured_log_dirs_passes_through() {
let raw = Some("/var/log,~/.local/state/dracon".to_string());
assert_eq!(
effective_log_dirs(&raw).as_deref(),
Some("/var/log,~/.local/state/dracon")
);
}
#[test]
fn the_default_log_dirs_is_the_guards_own_state_directory() {
let default = default_log_dirs();
assert_eq!(default, "~/.local/state/dracon");
let unit = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("dracon-system-guard.service"),
)
.expect("guard unit must be readable");
assert!(
unit.contains("%h/.local/state/dracon"),
"the default log dir must be inside the unit's ReadWritePaths"
);
}
#[test]
fn absent_and_blank_behave_differently_end_to_end_through_toml() {
let absent: SystemPolicy =
toml::from_str("[guard]\nmonitor_logs = true\n").expect("absent must parse");
let blank: SystemPolicy =
toml::from_str("[guard]\nmonitor_logs = true\nlog_dirs = \"\"\n").expect("blank parses");
assert_eq!(absent.guard.log_dirs, None);
assert_eq!(blank.guard.log_dirs, Some(String::new()));
assert_eq!(
effective_log_dirs(&absent.guard.log_dirs),
Some(default_log_dirs())
);
assert_eq!(effective_log_dirs(&blank.guard.log_dirs), None);
}
fn doc_of(toml_src: &str) -> toml::Value {
toml_src.parse().expect("fixture must be valid TOML")
}
#[test]
fn a_misspelled_knob_is_surfaced() {
let doc = doc_of("[guard]\ndisk_warn_persent = 80\n");
assert_eq!(
unknown_policy_keys(&doc),
vec!["guard.disk_warn_persent".to_string()],
"a typo must be reported, not dropped"
);
}
#[test]
fn a_key_under_the_wrong_table_is_surfaced() {
let doc = doc_of("[storage]\nmin_size_mb = 512\ncleanup_min_size_mb = 256\n");
let unknown = unknown_policy_keys(&doc);
assert!(
unknown.contains(&"storage.cleanup_min_size_mb".to_string()),
"a [guard] knob filed under [storage] must be surfaced, got {unknown:?}"
);
assert!(!unknown.is_empty());
}
#[test]
fn an_entirely_unknown_section_is_surfaced() {
let doc = doc_of("[guards]\ninterval_secs = 30\n");
assert_eq!(unknown_policy_keys(&doc), vec!["guards".to_string()]);
}
#[test]
fn a_section_written_as_a_bare_key_is_surfaced() {
let doc = doc_of("storage = 5\n");
assert_eq!(unknown_policy_keys(&doc), vec!["storage".to_string()]);
}
#[test]
fn a_fully_valid_policy_reports_nothing() {
let src = "[guard]\ninterval_secs = 30\ndisk_warn_percent = 80\n\n\
[storage]\nmin_size_mb = 512\nkinds = \"rust-build\"\n\n\
[[links.entries]]\nlink = \"/a\"\ntarget = \"/b\"\n";
assert!(
unknown_policy_keys(&doc_of(src)).is_empty(),
"a correct policy must not produce warnings"
);
}
#[test]
fn the_shipped_example_template_has_no_unknown_keys() {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("dracon-system.example.toml"),
)
.expect("example template must be readable");
let unknown = unknown_policy_keys(&text.parse().expect("template must parse"));
assert!(
unknown.is_empty(),
"dracon-system.example.toml contains keys the parser does not accept: {unknown:?}"
);
}
#[test]
fn the_known_key_set_is_derived_from_the_structs() {
let known = known_policy_keys();
assert!(known.contains_key("guard"), "guard section must be known");
let guard = &known["guard"];
for knob in [
"interval_secs",
"disk_warn_percent",
"log_dirs",
"notify_command",
] {
assert!(guard.contains(knob), "{knob} must be an accepted guard key");
}
assert!(
known["storage"].contains("min_size_mb"),
"storage keys must be derived too"
);
assert!(known["links"].contains("entries"));
assert!(!guard.contains("definitely_not_a_knob"));
}
#[test]
fn a_near_miss_spelling_gets_a_suggestion() {
assert!(hint_for("guard.disk_warn_persent").contains("disk_warn_percent"));
assert!(hint_for("guard.nonsense_key_xyz").is_empty());
}
#[test]
fn a_correctly_spelled_key_in_the_wrong_table_names_its_table() {
let hint = hint_for("storage.cleanup_min_size_mb");
assert!(
hint.contains("[guard]"),
"a misplaced key must be told which table it belongs under, got {hint:?}"
);
}
#[test]
fn option_field_keys_cover_every_option_field() {
let guard = known_policy_keys();
let guard = &guard["guard"];
for field in GUARD_OPTION_FIELDS {
assert!(
guard.contains(*field),
"{field} is an Option field and must be listed in GUARD_OPTION_FIELDS, \
or it will be reported as an unknown key on every load"
);
}
let doc = doc_of("[guard]\nlog_dirs = \"/var/log\"\n");
assert!(
unknown_policy_keys(&doc).is_empty(),
"a policy that sets the Option field must be clean, got {:?}",
unknown_policy_keys(&doc)
);
}
#[test]
fn percent_thresholds_keep_a_one_percent_floor() {
let mut p = GuardPolicy {
unfreeze_below_percent: 0,
disk_early_warn_percent: 0,
..GuardPolicy::default()
};
let adjusted = normalize_guard_policy(&mut p);
assert_eq!(
p.unfreeze_below_percent, 1,
"0% unfreeze would never lift the freeze; adjusted={adjusted:?}"
);
assert_eq!(
p.disk_early_warn_percent, 1,
"a 0% early warn fires on every pass; adjusted={adjusted:?}"
);
assert!(adjusted.contains(&"unfreeze_below_percent"));
assert!(adjusted.contains(&"disk_early_warn_percent"));
let mut pathological = GuardPolicy {
disk_warn_percent: 1,
disk_early_warn_percent: 1,
disk_action_percent: 1,
disk_critical_percent: 1,
unfreeze_below_percent: 1,
..GuardPolicy::default()
};
normalize_guard_policy(&mut pathological);
assert!(
pathological.unfreeze_below_percent >= 1,
"got {}",
pathological.unfreeze_below_percent
);
assert!(
pathological.proactive_cleanup_percent >= 1,
"got {}",
pathological.proactive_cleanup_percent
);
}
#[test]
fn notify_cooldown_has_a_ceiling() {
let mut p = GuardPolicy {
notify_cooldown_secs: u64::MAX,
..GuardPolicy::default()
};
let adjusted = normalize_guard_policy(&mut p);
assert!(
p.notify_cooldown_secs <= 86_400,
"notify_cooldown_secs must be capped, got {}",
p.notify_cooldown_secs
);
assert!(adjusted.contains(&"notify_cooldown_secs"));
}
#[test]
fn unknown_keys_inside_link_entries_are_reported() {
let doc: toml::Value = r#"
[links]
[[links.entries]]
link = "/a"
target = "/b"
targt = "/c"
"#
.parse()
.expect("fixture parses");
let unknown = unknown_policy_keys(&doc);
assert!(
unknown.contains(&"links.entries.targt".to_string()),
"a typo inside a link entry must be named, got {unknown:?}"
);
let clean: toml::Value = r#"
[links]
[[links.entries]]
link = "/a"
target = "/b"
"#
.parse()
.expect("fixture parses");
assert!(
unknown_policy_keys(&clean).is_empty(),
"a valid link entry must not be reported: {:?}",
unknown_policy_keys(&clean)
);
}
#[test]
fn a_mistyped_section_name_is_hinted() {
let hint = hint_for("guards");
assert!(
hint.contains("[guard]"),
"a mistyped section must be pointed at the real one, got {hint:?}"
);
}
#[test]
fn a_near_miss_hint_stays_inside_the_typed_family() {
let hint = hint_for("rust_target_action_min_age_day");
assert!(
hint.contains("rust_target_action_min_age_days"),
"the same-tier key must be suggested, got {hint:?}"
);
assert!(
!hint.contains("rust_target_max_age_days"),
"the max-age gate is a DIFFERENT setting and must not be suggested here: {hint:?}"
);
let unknown_tier = hint_for("rust_target_min_age_days");
assert!(
!unknown_tier.contains("did you mean rust_target_max_age_days"),
"a non-existent tier must not be presented as the max-age knob: {unknown_tier:?}"
);
assert!(
unknown_tier.contains("rust_target_max_age_days")
&& unknown_tier.contains("rust_target_action_min_age_days"),
"both real family members should be offered: {unknown_tier:?}"
);
let unrelated = hint_for("interval_sec");
assert!(
unrelated.contains("interval_secs"),
"an unrelated typo must still get a plain near miss, got {unrelated:?}"
);
}
fn shipped_unit() -> String {
std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("dracon-system-guard.service"),
)
.expect("guard unit must be readable")
}
#[test]
fn the_example_configs_storage_roots_are_inside_the_units_readwritepaths() {
let example = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("dracon-system.example.toml"),
)
.expect("example config must be readable");
let unit = shipped_unit();
let home = std::path::Path::new("/home/tester");
let mut checked = 0;
for line in example.lines() {
let line = line.trim_start_matches('#').trim();
let Some(rest) = line
.strip_prefix("quarantine_dir")
.or_else(|| line.strip_prefix("relocate_cold_root"))
else {
continue;
};
let Some(value) = rest.trim_start().strip_prefix('=') else {
continue;
};
let value = value.trim().trim_matches('"');
if value.is_empty() {
continue;
}
let path = match value.strip_prefix("~/") {
Some(rest) => home.join(rest),
None => std::path::PathBuf::from(value),
};
assert!(
crate::safety::unit_grants_write(&unit, home, &path),
"the example config documents {value} ({}), but the shipped unit's \
ReadWritePaths does not grant it — writes there fail EROFS under \
ProtectSystem=strict",
path.display()
);
checked += 1;
}
assert!(
checked >= 2,
"expected both documented storage roots to be checked, saw {checked}"
);
}
#[test]
fn a_repointed_storage_root_outside_readwritepaths_is_reported() {
let guard = GuardPolicy {
relocate_cold_root: "/mnt/data/cold".to_string(),
..Default::default()
};
assert!(
crate::safety::uncovered_storage_roots(&shipped_unit(), &guard).is_empty(),
"the shipped unit grants every configured storage root"
);
let stripped = shipped_unit().replace(" -/mnt/data/quarantine -/mnt/data/cold", "");
assert!(
!stripped.contains("/mnt/data/cold"),
"the mutation must actually remove the entry"
);
let reported = crate::safety::uncovered_storage_roots(&stripped, &guard);
assert_eq!(
reported.len(),
1,
"expected exactly the cold root to be reported, got {reported:?}"
);
assert_eq!(reported[0].0, "relocate_cold_root");
assert_eq!(reported[0].1, std::path::Path::new("/mnt/data/cold"));
}
#[test]
fn unit_readwrite_paths_expands_home_and_keeps_optional_entries() {
let unit = "\
ReadWritePaths=%h/.dracon /tmp
ReadWritePaths=-%h/Dev -/mnt/data/quarantine
";
let home = std::path::Path::new("/home/tester");
let granted = crate::safety::unit_readwrite_paths(unit, home);
assert!(
granted.contains(&"/home/tester/.dracon".into()),
"{granted:?}"
);
assert!(granted.contains(&"/home/tester/Dev".into()), "{granted:?}");
assert!(
granted.contains(&"/mnt/data/quarantine".into()),
"a '-' prefix means 'ignore if missing', not 'never granted': {granted:?}"
);
assert!(granted.contains(&"/tmp".into()), "{granted:?}");
assert!(crate::safety::unit_grants_write(
unit,
home,
std::path::Path::new("/home/tester/.dracon")
));
assert!(crate::safety::unit_grants_write(
unit,
home,
std::path::Path::new("/home/tester/Dev/a-repo/target")
));
assert!(crate::safety::unit_grants_write(
unit,
home,
std::path::Path::new("/tmp/nested/file")
));
assert!(
!crate::safety::unit_grants_write(unit, home, std::path::Path::new("/mnt/data/cold")),
"an unlisted path is not covered"
);
assert!(
!crate::safety::unit_grants_write(
unit,
home,
std::path::Path::new("/home/tester/.local/state/dracon")
),
"a sibling of a granted path is not covered"
);
}
#[test]
fn protected_paths_tilde_entry_actually_protects() {
let root = std::env::temp_dir().join(format!(
"dracon-protected-tilde-{}-{}",
std::process::id(),
"tilde"
));
let _ = std::fs::remove_dir_all(&root);
let fake_home = root.join("home");
let protected_tree = fake_home.join("Dev/dracon-utilities/target");
std::fs::create_dir_all(&protected_tree).unwrap();
std::fs::write(protected_tree.join("a.txt"), b"keep").unwrap();
let other_tree = fake_home.join("Dev/some-other-repo/target");
std::fs::create_dir_all(&other_tree).unwrap();
std::fs::write(other_tree.join("b.txt"), b"reclaimable").unwrap();
let toml_src = r#"
[guard]
protected_paths = ["~/Dev/dracon-utilities"]
"#;
let mut parsed: SystemPolicy = toml::from_str(toml_src).expect("fixture must parse");
assert_eq!(
parsed.guard.protected_paths,
vec!["~/Dev/dracon-utilities".to_string()],
"precondition: the raw policy really does carry the tilde form"
);
normalize_guard_policy_with_home(&mut parsed.guard, Some(&fake_home));
assert_eq!(
parsed.guard.protected_paths,
vec![fake_home.join("Dev/dracon-utilities").display().to_string()],
"the tilde form must be expanded to an absolute path"
);
assert!(
!normalize_guard_policy_with_home(&mut parsed.guard, Some(&fake_home))
.contains(&"protected_paths"),
"a legal tilde form must not be reported as clamped"
);
let err = crate::check_safe_to_delete_guard(&protected_tree, &parsed.guard.protected_paths)
.expect_err("a ~ entry must protect, not silently no-op");
let msg = format!("{err:#}");
assert!(
msg.contains("refusing to delete protected path"),
"expected a protection refusal, got: {msg}"
);
assert!(
crate::check_safe_to_delete_guard(&other_tree, &parsed.guard.protected_paths).is_ok(),
"protection must stay scoped to the configured ancestor"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn protected_paths_expansion_is_idempotent() {
let home = Path::new("/tmp/fake-home-for-idempotence");
let mut p = GuardPolicy {
protected_paths: vec![
"~/Dev/dracon-utilities".to_string(),
"/already/absolute".to_string(),
],
..Default::default()
};
normalize_guard_policy_with_home(&mut p, Some(home));
let snapshot = p.protected_paths.clone();
let second = normalize_guard_policy_with_home(&mut p, Some(home));
assert!(
!second.contains(&"protected_paths"),
"a second pass must find nothing to adjust"
);
assert_eq!(p.protected_paths, snapshot);
assert_eq!(
p.protected_paths[1], "/already/absolute",
"an absolute entry must not be rewritten"
);
}
#[test]
fn unresolvable_protected_entry_warns_once_and_does_not_refuse() {
let pid = std::process::id();
let missing = |which: &str| format!("/tmp/dracon-missing-protected-{pid}-{which}");
let probe = |which: &str| {
let p = std::env::temp_dir().join(format!("dracon-failopen-probe-{pid}-{which}"));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
};
let a = missing("helper");
assert!(
!std::path::Path::new(&a).exists(),
"precondition: the entry must not resolve"
);
assert!(
crate::safety::note_unresolvable_protected(&a),
"first sighting warns"
);
assert!(
!crate::safety::note_unresolvable_protected(&a),
"a second sighting must be silent"
);
let b = missing("guard-arm");
let cand_b = probe("guard-arm");
assert!(
crate::check_safe_to_delete_guard(&cand_b, std::slice::from_ref(&b)).is_ok(),
"a typo in protected_paths must not block all cleanup"
);
assert!(
!crate::safety::note_unresolvable_protected(&b),
"the guard classifier must register an unresolvable protected entry"
);
let _ = std::fs::remove_dir_all(&cand_b);
let c = missing("strict-arm");
let cand_c = probe("strict-arm");
let _ = crate::check_safe_to_delete(&cand_c, std::slice::from_ref(&c));
assert!(
!crate::safety::note_unresolvable_protected(&c),
"the strict classifier must register an unresolvable protected entry"
);
let _ = std::fs::remove_dir_all(&cand_c);
}