1use crate::cli::CliOutput;
20use crate::{db, models};
21use anyhow::{Result, bail};
22use clap::Args;
23use models::Tier;
24use std::path::Path;
25
26#[derive(Args)]
27pub struct ForgetArgs {
28 #[arg(long, short)]
29 pub namespace: Option<String>,
30 #[arg(long, short)]
31 pub pattern: Option<String>,
32 #[arg(long, short)]
33 pub tier: Option<String>,
34 #[arg(long, default_value_t = false)]
39 pub confirm_global: bool,
40}
41
42#[must_use]
48pub fn global_scope_forget_error_message() -> &'static str {
49 "global-scope forget requires --confirm-global; restrict with --namespace=<ns> for safety"
50}
51
52#[must_use]
57pub fn requires_global_confirmation(args: &ForgetArgs) -> bool {
58 let no_namespace = args.namespace.is_none();
59 let has_global_filter = args.pattern.is_some() || args.tier.is_some();
60 no_namespace && has_global_filter && !args.confirm_global
61}
62
63pub fn cmd_forget(
66 db_path: &Path,
67 args: &ForgetArgs,
68 json_out: bool,
69 out: &mut CliOutput<'_>,
70) -> Result<()> {
71 if requires_global_confirmation(args) {
76 bail!(global_scope_forget_error_message());
77 }
78
79 let tier = args.tier.as_deref().and_then(Tier::from_str);
80 let conn = db::open(db_path)?;
81 match db::forget(
82 &conn,
83 args.namespace.as_deref(),
84 args.pattern.as_deref(),
85 tier.as_ref(),
86 true, ) {
88 Ok(n) => {
89 if json_out {
90 writeln!(out.stdout, "{}", serde_json::json!({"deleted": n}))?;
91 } else {
92 writeln!(out.stdout, "forgot {n} memories")?;
93 }
94 }
95 Err(e) => {
96 writeln!(out.stderr, "{}", crate::errors::msg::error_line(&e))?;
97 std::process::exit(1);
98 }
99 }
100 Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::cli::test_utils::{TestEnv, seed_memory};
107
108 fn args() -> ForgetArgs {
109 ForgetArgs {
110 namespace: None,
111 pattern: None,
112 tier: None,
113 confirm_global: false,
114 }
115 }
116
117 #[test]
118 fn test_forget_by_namespace() {
119 let mut env = TestEnv::fresh();
120 let db = env.db_path.clone();
121 let _ = seed_memory(&db, "alpha", "a", "ca");
122 let _ = seed_memory(&db, "beta", "b", "cb");
123 let mut a = args();
124 a.namespace = Some("alpha".to_string());
125 {
126 let mut out = env.output();
127 cmd_forget(&db, &a, true, &mut out).unwrap();
128 }
129 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
130 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
131 let conn = db::open(&db).unwrap();
133 let still = db::list(
134 &conn,
135 Some("beta"),
136 None,
137 10,
138 0,
139 None,
140 None,
141 None,
142 None,
143 None,
144 )
145 .unwrap();
146 assert_eq!(still.len(), 1);
147 }
148
149 #[test]
150 fn test_forget_by_pattern() {
151 let mut env = TestEnv::fresh();
152 let db = env.db_path.clone();
153 let _ = seed_memory(&db, "ns", "apple pie", "yum");
154 let _ = seed_memory(&db, "ns", "banana split", "also yum");
155 let mut a = args();
156 a.pattern = Some("apple".to_string());
157 a.confirm_global = true;
160 {
161 let mut out = env.output();
162 cmd_forget(&db, &a, true, &mut out).unwrap();
163 }
164 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
165 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
166 }
167
168 #[test]
169 fn test_forget_by_tier() {
170 let mut env = TestEnv::fresh();
171 let db = env.db_path.clone();
172 let id_long = seed_memory(&db, "ns", "long-row", "x");
173 let _ = seed_memory(&db, "ns", "mid-row", "y");
174 {
175 let conn = db::open(&db).unwrap();
176 db::update(
177 &conn,
178 &id_long,
179 None,
180 None,
181 Some(&Tier::Long),
182 None,
183 None,
184 None,
185 None,
186 None,
187 None,
188 )
189 .unwrap();
190 }
191 let mut a = args();
192 a.tier = Some(Tier::Long.as_str().to_string());
193 a.confirm_global = true;
196 {
197 let mut out = env.output();
198 cmd_forget(&db, &a, true, &mut out).unwrap();
199 }
200 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
201 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
202 }
203
204 #[test]
205 fn test_forget_combined_filters() {
206 let mut env = TestEnv::fresh();
207 let db = env.db_path.clone();
208 let _ = seed_memory(&db, "alpha", "apple-1", "x");
209 let _ = seed_memory(&db, "beta", "apple-2", "y");
210 let _ = seed_memory(&db, "alpha", "banana", "z");
211 let mut a = args();
212 a.namespace = Some("alpha".to_string());
213 a.pattern = Some("apple".to_string());
214 {
215 let mut out = env.output();
216 cmd_forget(&db, &a, true, &mut out).unwrap();
217 }
218 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
219 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
221 let conn = db::open(&db).unwrap();
222 let beta_apples = db::list(
223 &conn,
224 Some("beta"),
225 None,
226 10,
227 0,
228 None,
229 None,
230 None,
231 None,
232 None,
233 )
234 .unwrap();
235 assert_eq!(beta_apples.len(), 1);
236 }
237
238 #[test]
239 fn test_forget_no_filter_errors_or_no_op() {
240 let env = TestEnv::fresh();
245 let db = env.db_path.clone();
246 let _ = seed_memory(&db, "ns", "x", "y");
247 let conn = db::open(&db).unwrap();
248 let res = db::forget(&conn, None, None, None, false);
249 assert!(res.is_err(), "no-filter forget must error");
250 assert!(
251 res.unwrap_err()
252 .to_string()
253 .contains("at least one of namespace, pattern, or tier")
254 );
255 }
256
257 #[test]
260 fn requires_global_confirmation_pattern_no_namespace() {
261 let mut a = args();
262 a.pattern = Some("apple".into());
263 assert!(requires_global_confirmation(&a));
264 }
265
266 #[test]
267 fn requires_global_confirmation_tier_no_namespace() {
268 let mut a = args();
269 a.tier = Some(Tier::Long.as_str().into());
270 assert!(requires_global_confirmation(&a));
271 }
272
273 #[test]
274 fn does_not_require_confirmation_when_namespace_present() {
275 let mut a = args();
276 a.namespace = Some("ns".into());
277 a.pattern = Some("apple".into());
278 assert!(!requires_global_confirmation(&a));
279 }
280
281 #[test]
282 fn does_not_require_confirmation_when_only_namespace_set() {
283 let mut a = args();
284 a.namespace = Some("ns".into());
285 assert!(!requires_global_confirmation(&a));
287 }
288
289 #[test]
290 fn does_not_require_confirmation_when_confirm_flag_set() {
291 let mut a = args();
292 a.pattern = Some("apple".into());
293 a.confirm_global = true;
294 assert!(!requires_global_confirmation(&a));
295 }
296
297 #[test]
298 fn cmd_forget_refuses_global_pattern_without_confirm() {
299 let mut env = TestEnv::fresh();
300 let db = env.db_path.clone();
301 let _ = seed_memory(&db, "ns", "apple pie", "yum");
302 let mut a = args();
303 a.pattern = Some("apple".into());
304 let mut out = env.output();
305 let res = cmd_forget(&db, &a, true, &mut out);
306 assert!(res.is_err(), "expected refusal");
307 let msg = res.unwrap_err().to_string();
308 assert!(msg.contains("--confirm-global"), "got: {msg}");
309 }
310
311 #[test]
312 fn cmd_forget_proceeds_with_confirm_global() {
313 let mut env = TestEnv::fresh();
314 let db = env.db_path.clone();
315 let _ = seed_memory(&db, "ns", "apple pie", "yum");
316 let _ = seed_memory(&db, "other", "apple cake", "yum");
317 let mut a = args();
318 a.pattern = Some("apple".into());
319 a.confirm_global = true;
320 {
321 let mut out = env.output();
322 cmd_forget(&db, &a, true, &mut out).unwrap();
323 }
324 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
325 assert_eq!(v["deleted"].as_u64().unwrap(), 2);
328 }
329
330 #[test]
331 fn test_forget_text_output_count() {
332 let mut env = TestEnv::fresh();
333 let db = env.db_path.clone();
334 let _ = seed_memory(&db, "ns", "a", "x");
335 let _ = seed_memory(&db, "ns", "b", "y");
336 let mut a = args();
337 a.namespace = Some("ns".to_string());
338 {
339 let mut out = env.output();
340 cmd_forget(&db, &a, false, &mut out).unwrap();
341 }
342 let stdout = env.stdout_str();
343 assert!(stdout.contains("forgot 2 memories"), "got: {stdout}");
344 }
345}