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",
];
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);
}