use bpaf::{Parser, construct, long, pure};
use haz_domain::settings::cache_clean::max_age::MaxAge;
use haz_domain::settings::cache_clean::max_size::MaxSize;
use crate::cli::Cmd;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CacheCleanArgs {
pub soft: bool,
pub max_age: Option<MaxAge>,
pub max_size: Option<MaxSize>,
pub dry_run: bool,
}
pub(super) fn cache_command() -> impl Parser<Cmd> {
let remove_all = pure(Cmd::CacheClear)
.to_options()
.command("clear")
.help("Remove every cache entry (CACHE-021)");
let prune_stale = clean_command()
.to_options()
.command("clean")
.help("Selectively reclaim cache entries (CACHE-022, AUX-022..AUX-027)");
let report_info = pure(Cmd::CacheInfo)
.to_options()
.command("info")
.help("Report cache-state statistics (AUX-017..AUX-021)");
construct!([remove_all, prune_stale, report_info])
}
fn clean_command() -> impl Parser<Cmd> {
let soft = long("soft")
.help("Reclaim objectively-stale state (CACHE-022)")
.switch();
let max_age = long("max-age")
.help("Evict entries strictly older than DURATION (AUX-022)")
.argument::<String>("DURATION")
.parse(|s| MaxAge::parse(&s).map_err(|e| e.to_string()))
.optional();
let max_size = long("max-size")
.help("Cap on-disk footprint at BYTES, evicting oldest first (AUX-022)")
.argument::<String>("BYTES")
.parse(|s| MaxSize::parse(&s).map_err(|e| e.to_string()))
.optional();
let dry_run = long("dry-run")
.help("Compute the eviction set without removing anything (AUX-024)")
.switch();
construct!(CacheCleanArgs {
soft,
max_age,
max_size,
dry_run,
})
.map(Cmd::CacheClean)
}
#[cfg(test)]
mod tests {
use super::{CacheCleanArgs, MaxAge, MaxSize};
use crate::cli::{Cmd, parse};
#[test]
fn cache_clear_parses() {
assert_eq!(parse(&["cache", "clear"]).unwrap(), Cmd::CacheClear);
}
#[test]
fn cache_info_parses() {
assert_eq!(parse(&["cache", "info"]).unwrap(), Cmd::CacheInfo);
}
#[test]
fn aux_017_cache_info_rejects_positional() {
let err = parse(&["cache", "info", "extra"]).unwrap_err();
assert!(
!err.is_empty(),
"extra positional must produce a diagnostic: {err}",
);
}
#[test]
fn aux_017_cache_info_rejects_unknown_flag() {
let err = parse(&["cache", "info", "--soft"]).unwrap_err();
assert!(
!err.is_empty(),
"unknown flag must produce a diagnostic: {err}",
);
}
#[test]
fn cache_clean_soft_parses() {
assert_eq!(
parse(&["cache", "clean", "--soft"]).unwrap(),
Cmd::CacheClean(CacheCleanArgs {
soft: true,
..CacheCleanArgs::default()
}),
);
}
#[test]
fn cache_clean_bare_parses_with_no_modes() {
assert_eq!(
parse(&["cache", "clean"]).unwrap(),
Cmd::CacheClean(CacheCleanArgs::default()),
);
}
#[test]
fn cache_clean_max_age_parses_valid_duration() {
let want = MaxAge::parse("90d").unwrap();
assert_eq!(
parse(&["cache", "clean", "--max-age", "90d"]).unwrap(),
Cmd::CacheClean(CacheCleanArgs {
max_age: Some(want),
..CacheCleanArgs::default()
}),
);
}
#[test]
fn cache_clean_max_age_rejects_bare_integer() {
let err = parse(&["cache", "clean", "--max-age", "90"]).unwrap_err();
assert!(
!err.is_empty(),
"malformed --max-age must produce a diagnostic: {err}",
);
}
#[test]
fn cache_clean_max_size_parses_si_suffix() {
let want = MaxSize::parse("1GB").unwrap();
assert_eq!(
parse(&["cache", "clean", "--max-size", "1GB"]).unwrap(),
Cmd::CacheClean(CacheCleanArgs {
max_size: Some(want),
..CacheCleanArgs::default()
}),
);
}
#[test]
fn cache_clean_max_size_rejects_unknown_suffix() {
let err = parse(&["cache", "clean", "--max-size", "1ZB"]).unwrap_err();
assert!(
!err.is_empty(),
"malformed --max-size must produce a diagnostic: {err}",
);
}
#[test]
fn cache_clean_dry_run_composes_with_other_flags() {
let want_age = MaxAge::parse("30d").unwrap();
assert_eq!(
parse(&["cache", "clean", "--soft", "--max-age", "30d", "--dry-run"]).unwrap(),
Cmd::CacheClean(CacheCleanArgs {
soft: true,
max_age: Some(want_age),
max_size: None,
dry_run: true,
}),
);
}
}