1use bpaf::{Parser, construct, long, pure};
10use haz_domain::settings::cache_clean::max_age::MaxAge;
11use haz_domain::settings::cache_clean::max_size::MaxSize;
12
13use crate::cli::Cmd;
14
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
27pub struct CacheCleanArgs {
28 pub soft: bool,
30 pub max_age: Option<MaxAge>,
33 pub max_size: Option<MaxSize>,
37 pub dry_run: bool,
40}
41
42pub(super) fn cache_command() -> impl Parser<Cmd> {
43 let remove_all = pure(Cmd::CacheClear)
51 .to_options()
52 .command("clear")
53 .help("Remove every cache entry (CACHE-021)");
54 let prune_stale = clean_command()
55 .to_options()
56 .command("clean")
57 .help("Selectively reclaim cache entries (CACHE-022, AUX-022..AUX-027)");
58 let report_info = pure(Cmd::CacheInfo)
59 .to_options()
60 .command("info")
61 .help("Report cache-state statistics (AUX-017..AUX-021)");
62 construct!([remove_all, prune_stale, report_info])
63}
64
65fn clean_command() -> impl Parser<Cmd> {
66 let soft = long("soft")
71 .help("Reclaim objectively-stale state (CACHE-022)")
72 .switch();
73 let max_age = long("max-age")
74 .help("Evict entries strictly older than DURATION (AUX-022)")
75 .argument::<String>("DURATION")
76 .parse(|s| MaxAge::parse(&s).map_err(|e| e.to_string()))
77 .optional();
78 let max_size = long("max-size")
79 .help("Cap on-disk footprint at BYTES, evicting oldest first (AUX-022)")
80 .argument::<String>("BYTES")
81 .parse(|s| MaxSize::parse(&s).map_err(|e| e.to_string()))
82 .optional();
83 let dry_run = long("dry-run")
84 .help("Compute the eviction set without removing anything (AUX-024)")
85 .switch();
86 construct!(CacheCleanArgs {
87 soft,
88 max_age,
89 max_size,
90 dry_run,
91 })
92 .map(Cmd::CacheClean)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::{CacheCleanArgs, MaxAge, MaxSize};
98 use crate::cli::{Cmd, parse};
99
100 #[test]
101 fn cache_clear_parses() {
102 assert_eq!(parse(&["cache", "clear"]).unwrap(), Cmd::CacheClear);
103 }
104
105 #[test]
106 fn cache_info_parses() {
107 assert_eq!(parse(&["cache", "info"]).unwrap(), Cmd::CacheInfo);
108 }
109
110 #[test]
111 fn aux_017_cache_info_rejects_positional() {
112 let err = parse(&["cache", "info", "extra"]).unwrap_err();
115 assert!(
116 !err.is_empty(),
117 "extra positional must produce a diagnostic: {err}",
118 );
119 }
120
121 #[test]
122 fn aux_017_cache_info_rejects_unknown_flag() {
123 let err = parse(&["cache", "info", "--soft"]).unwrap_err();
125 assert!(
126 !err.is_empty(),
127 "unknown flag must produce a diagnostic: {err}",
128 );
129 }
130
131 #[test]
132 fn cache_clean_soft_parses() {
133 assert_eq!(
134 parse(&["cache", "clean", "--soft"]).unwrap(),
135 Cmd::CacheClean(CacheCleanArgs {
136 soft: true,
137 ..CacheCleanArgs::default()
138 }),
139 );
140 }
141
142 #[test]
143 fn cache_clean_bare_parses_with_no_modes() {
144 assert_eq!(
149 parse(&["cache", "clean"]).unwrap(),
150 Cmd::CacheClean(CacheCleanArgs::default()),
151 );
152 }
153
154 #[test]
155 fn cache_clean_max_age_parses_valid_duration() {
156 let want = MaxAge::parse("90d").unwrap();
157 assert_eq!(
158 parse(&["cache", "clean", "--max-age", "90d"]).unwrap(),
159 Cmd::CacheClean(CacheCleanArgs {
160 max_age: Some(want),
161 ..CacheCleanArgs::default()
162 }),
163 );
164 }
165
166 #[test]
167 fn cache_clean_max_age_rejects_bare_integer() {
168 let err = parse(&["cache", "clean", "--max-age", "90"]).unwrap_err();
171 assert!(
172 !err.is_empty(),
173 "malformed --max-age must produce a diagnostic: {err}",
174 );
175 }
176
177 #[test]
178 fn cache_clean_max_size_parses_si_suffix() {
179 let want = MaxSize::parse("1GB").unwrap();
180 assert_eq!(
181 parse(&["cache", "clean", "--max-size", "1GB"]).unwrap(),
182 Cmd::CacheClean(CacheCleanArgs {
183 max_size: Some(want),
184 ..CacheCleanArgs::default()
185 }),
186 );
187 }
188
189 #[test]
190 fn cache_clean_max_size_rejects_unknown_suffix() {
191 let err = parse(&["cache", "clean", "--max-size", "1ZB"]).unwrap_err();
192 assert!(
193 !err.is_empty(),
194 "malformed --max-size must produce a diagnostic: {err}",
195 );
196 }
197
198 #[test]
199 fn cache_clean_dry_run_composes_with_other_flags() {
200 let want_age = MaxAge::parse("30d").unwrap();
201 assert_eq!(
202 parse(&["cache", "clean", "--soft", "--max-age", "30d", "--dry-run"]).unwrap(),
203 Cmd::CacheClean(CacheCleanArgs {
204 soft: true,
205 max_age: Some(want_age),
206 max_size: None,
207 dry_run: true,
208 }),
209 );
210 }
211}