1use crate::cli::CliOutput;
7use crate::{db, models};
8use anyhow::Result;
9use clap::Args;
10use models::Tier;
11use std::path::Path;
12
13#[derive(Args)]
14pub struct ForgetArgs {
15 #[arg(long, short)]
16 pub namespace: Option<String>,
17 #[arg(long, short)]
18 pub pattern: Option<String>,
19 #[arg(long, short)]
20 pub tier: Option<String>,
21}
22
23pub fn cmd_forget(
26 db_path: &Path,
27 args: &ForgetArgs,
28 json_out: bool,
29 out: &mut CliOutput<'_>,
30) -> Result<()> {
31 let tier = args.tier.as_deref().and_then(Tier::from_str);
32 let conn = db::open(db_path)?;
33 match db::forget(
34 &conn,
35 args.namespace.as_deref(),
36 args.pattern.as_deref(),
37 tier.as_ref(),
38 true, ) {
40 Ok(n) => {
41 if json_out {
42 writeln!(out.stdout, "{}", serde_json::json!({"deleted": n}))?;
43 } else {
44 writeln!(out.stdout, "forgot {n} memories")?;
45 }
46 }
47 Err(e) => {
48 writeln!(out.stderr, "error: {e}")?;
49 std::process::exit(1);
50 }
51 }
52 Ok(())
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::cli::test_utils::{TestEnv, seed_memory};
59
60 fn args() -> ForgetArgs {
61 ForgetArgs {
62 namespace: None,
63 pattern: None,
64 tier: None,
65 }
66 }
67
68 #[test]
69 fn test_forget_by_namespace() {
70 let mut env = TestEnv::fresh();
71 let db = env.db_path.clone();
72 let _ = seed_memory(&db, "alpha", "a", "ca");
73 let _ = seed_memory(&db, "beta", "b", "cb");
74 let mut a = args();
75 a.namespace = Some("alpha".to_string());
76 {
77 let mut out = env.output();
78 cmd_forget(&db, &a, true, &mut out).unwrap();
79 }
80 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
81 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
82 let conn = db::open(&db).unwrap();
84 let still = db::list(
85 &conn,
86 Some("beta"),
87 None,
88 10,
89 0,
90 None,
91 None,
92 None,
93 None,
94 None,
95 )
96 .unwrap();
97 assert_eq!(still.len(), 1);
98 }
99
100 #[test]
101 fn test_forget_by_pattern() {
102 let mut env = TestEnv::fresh();
103 let db = env.db_path.clone();
104 let _ = seed_memory(&db, "ns", "apple pie", "yum");
105 let _ = seed_memory(&db, "ns", "banana split", "also yum");
106 let mut a = args();
107 a.pattern = Some("apple".to_string());
108 {
109 let mut out = env.output();
110 cmd_forget(&db, &a, true, &mut out).unwrap();
111 }
112 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
113 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
114 }
115
116 #[test]
117 fn test_forget_by_tier() {
118 let mut env = TestEnv::fresh();
119 let db = env.db_path.clone();
120 let id_long = seed_memory(&db, "ns", "long-row", "x");
121 let _ = seed_memory(&db, "ns", "mid-row", "y");
122 {
123 let conn = db::open(&db).unwrap();
124 db::update(
125 &conn,
126 &id_long,
127 None,
128 None,
129 Some(&Tier::Long),
130 None,
131 None,
132 None,
133 None,
134 None,
135 None,
136 )
137 .unwrap();
138 }
139 let mut a = args();
140 a.tier = Some("long".to_string());
141 {
142 let mut out = env.output();
143 cmd_forget(&db, &a, true, &mut out).unwrap();
144 }
145 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
146 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
147 }
148
149 #[test]
150 fn test_forget_combined_filters() {
151 let mut env = TestEnv::fresh();
152 let db = env.db_path.clone();
153 let _ = seed_memory(&db, "alpha", "apple-1", "x");
154 let _ = seed_memory(&db, "beta", "apple-2", "y");
155 let _ = seed_memory(&db, "alpha", "banana", "z");
156 let mut a = args();
157 a.namespace = Some("alpha".to_string());
158 a.pattern = Some("apple".to_string());
159 {
160 let mut out = env.output();
161 cmd_forget(&db, &a, true, &mut out).unwrap();
162 }
163 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
164 assert_eq!(v["deleted"].as_u64().unwrap(), 1);
166 let conn = db::open(&db).unwrap();
167 let beta_apples = db::list(
168 &conn,
169 Some("beta"),
170 None,
171 10,
172 0,
173 None,
174 None,
175 None,
176 None,
177 None,
178 )
179 .unwrap();
180 assert_eq!(beta_apples.len(), 1);
181 }
182
183 #[test]
184 fn test_forget_no_filter_errors_or_no_op() {
185 let env = TestEnv::fresh();
190 let db = env.db_path.clone();
191 let _ = seed_memory(&db, "ns", "x", "y");
192 let conn = db::open(&db).unwrap();
193 let res = db::forget(&conn, None, None, None, false);
194 assert!(res.is_err(), "no-filter forget must error");
195 assert!(
196 res.unwrap_err()
197 .to_string()
198 .contains("at least one of namespace, pattern, or tier")
199 );
200 }
201
202 #[test]
203 fn test_forget_text_output_count() {
204 let mut env = TestEnv::fresh();
205 let db = env.db_path.clone();
206 let _ = seed_memory(&db, "ns", "a", "x");
207 let _ = seed_memory(&db, "ns", "b", "y");
208 let mut a = args();
209 a.namespace = Some("ns".to_string());
210 {
211 let mut out = env.output();
212 cmd_forget(&db, &a, false, &mut out).unwrap();
213 }
214 let stdout = env.stdout_str();
215 assert!(stdout.contains("forgot 2 memories"), "got: {stdout}");
216 }
217}