pub(crate) fn budget_dry_run_env_is_unhonoured(
env_value: Option<&str>,
has_disk_budget: bool,
) -> Option<String> {
let requested = env_value.is_some_and(|v| v != "0");
if !requested || !has_disk_budget {
return None;
}
Some(
"FORJAR_BUDGET_DRY_RUN is set, but this apply would reclaim disk anyway. \
That variable belongs to the generated reaper and is read on the TARGET; \
it survives neither `sudo` (env_reset) nor `ssh` (no SendEnv), so forjar \
cannot honour it and will not pretend to. Two previews do work: \
`forjar apply --dry-run` plans and executes nothing, and \
`forjar codegen -r <id> --phase reaper > /tmp/reaper.sh && sh /tmp/reaper.sh` \
runs the real reclaim logic in preview mode and deletes nothing. \
Unset FORJAR_BUDGET_DRY_RUN to apply for real."
.to_string(),
)
}
pub(crate) fn scope_holds_a_disk_budget(
config: &crate::core::types::ForjarConfig,
machine_filter: Option<&str>,
resource_filter: Option<&str>,
tag_filter: Option<&str>,
) -> bool {
config.resources.iter().any(|(id, r)| {
r.resource_type == crate::core::types::ResourceType::DiskBudget
&& resource_filter.is_none_or(|f| id == f)
&& tag_filter.is_none_or(|t| r.tags.iter().any(|x| x == t))
&& machine_filter.is_none_or(|m| r.machine.iter().any(|x| x == m))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{MachineTarget, Resource, ResourceType};
#[test]
fn refuses_when_a_budget_is_in_scope() {
let msg = budget_dry_run_env_is_unhonoured(Some("1"), true).expect("must refuse");
assert!(
msg.contains("forjar codegen"),
"must name a real preview: {msg}"
);
assert!(msg.contains("--dry-run"));
assert!(msg.contains("--phase reaper"));
}
#[test]
fn zero_is_not_a_request() {
assert!(budget_dry_run_env_is_unhonoured(Some("0"), true).is_none());
}
#[test]
fn unset_is_not_a_request() {
assert!(budget_dry_run_env_is_unhonoured(None, true).is_none());
}
#[test]
fn does_not_block_an_apply_that_holds_no_budget() {
assert!(budget_dry_run_env_is_unhonoured(Some("1"), false).is_none());
}
fn cfg(kinds: &[(&str, ResourceType)]) -> crate::core::types::ForjarConfig {
let mut c = crate::core::types::ForjarConfig::default();
for (id, t) in kinds {
c.resources.insert(
(*id).to_string(),
Resource {
resource_type: t.clone(),
machine: MachineTarget::Single("intel".into()),
tags: vec!["disk".into()],
..Default::default()
},
);
}
c
}
#[test]
fn scope_sees_a_budget_only_when_the_filters_select_it() {
let c = cfg(&[
("pkg", ResourceType::Package),
("budget", ResourceType::DiskBudget),
]);
assert!(scope_holds_a_disk_budget(&c, None, None, None));
assert!(scope_holds_a_disk_budget(&c, None, Some("budget"), None));
assert!(!scope_holds_a_disk_budget(&c, None, Some("pkg"), None));
assert!(scope_holds_a_disk_budget(&c, Some("intel"), None, None));
assert!(!scope_holds_a_disk_budget(&c, Some("lambda"), None, None));
assert!(scope_holds_a_disk_budget(&c, None, None, Some("disk")));
assert!(!scope_holds_a_disk_budget(&c, None, None, Some("web")));
}
#[test]
fn a_config_with_no_budget_is_never_in_scope() {
let c = cfg(&[("pkg", ResourceType::Package)]);
assert!(!scope_holds_a_disk_budget(&c, None, None, None));
}
}