use crate::cli::CliOutput;
use crate::{db, models};
use anyhow::{Result, bail};
use clap::Args;
use models::Tier;
use std::path::Path;
#[derive(Args)]
pub struct ForgetArgs {
#[arg(long, short)]
pub namespace: Option<String>,
#[arg(long, short)]
pub pattern: Option<String>,
#[arg(long, short)]
pub tier: Option<String>,
#[arg(long, default_value_t = false)]
pub confirm_global: bool,
}
#[must_use]
pub fn global_scope_forget_error_message() -> &'static str {
"global-scope forget requires --confirm-global; restrict with --namespace=<ns> for safety"
}
#[must_use]
pub fn requires_global_confirmation(args: &ForgetArgs) -> bool {
let no_namespace = args.namespace.is_none();
let has_global_filter = args.pattern.is_some() || args.tier.is_some();
no_namespace && has_global_filter && !args.confirm_global
}
pub fn cmd_forget(
db_path: &Path,
args: &ForgetArgs,
json_out: bool,
out: &mut CliOutput<'_>,
) -> Result<()> {
if requires_global_confirmation(args) {
bail!(global_scope_forget_error_message());
}
let tier = args.tier.as_deref().and_then(Tier::from_str);
let conn = db::open(db_path)?;
match db::forget(
&conn,
args.namespace.as_deref(),
args.pattern.as_deref(),
tier.as_ref(),
true, ) {
Ok(n) => {
if json_out {
writeln!(out.stdout, "{}", serde_json::json!({"deleted": n}))?;
} else {
writeln!(out.stdout, "forgot {n} memories")?;
}
}
Err(e) => {
writeln!(out.stderr, "{}", crate::errors::msg::error_line(&e))?;
std::process::exit(1);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
fn args() -> ForgetArgs {
ForgetArgs {
namespace: None,
pattern: None,
tier: None,
confirm_global: false,
}
}
#[test]
fn test_forget_by_namespace() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "alpha", "a", "ca");
let _ = seed_memory(&db, "beta", "b", "cb");
let mut a = args();
a.namespace = Some("alpha".to_string());
{
let mut out = env.output();
cmd_forget(&db, &a, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["deleted"].as_u64().unwrap(), 1);
let conn = db::open(&db).unwrap();
let still = db::list(
&conn,
Some("beta"),
None,
10,
0,
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(still.len(), 1);
}
#[test]
fn test_forget_by_pattern() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "ns", "apple pie", "yum");
let _ = seed_memory(&db, "ns", "banana split", "also yum");
let mut a = args();
a.pattern = Some("apple".to_string());
a.confirm_global = true;
{
let mut out = env.output();
cmd_forget(&db, &a, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["deleted"].as_u64().unwrap(), 1);
}
#[test]
fn test_forget_by_tier() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let id_long = seed_memory(&db, "ns", "long-row", "x");
let _ = seed_memory(&db, "ns", "mid-row", "y");
{
let conn = db::open(&db).unwrap();
db::update(
&conn,
&id_long,
None,
None,
Some(&Tier::Long),
None,
None,
None,
None,
None,
None,
)
.unwrap();
}
let mut a = args();
a.tier = Some(Tier::Long.as_str().to_string());
a.confirm_global = true;
{
let mut out = env.output();
cmd_forget(&db, &a, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["deleted"].as_u64().unwrap(), 1);
}
#[test]
fn test_forget_combined_filters() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "alpha", "apple-1", "x");
let _ = seed_memory(&db, "beta", "apple-2", "y");
let _ = seed_memory(&db, "alpha", "banana", "z");
let mut a = args();
a.namespace = Some("alpha".to_string());
a.pattern = Some("apple".to_string());
{
let mut out = env.output();
cmd_forget(&db, &a, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["deleted"].as_u64().unwrap(), 1);
let conn = db::open(&db).unwrap();
let beta_apples = db::list(
&conn,
Some("beta"),
None,
10,
0,
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(beta_apples.len(), 1);
}
#[test]
fn test_forget_no_filter_errors_or_no_op() {
let env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "ns", "x", "y");
let conn = db::open(&db).unwrap();
let res = db::forget(&conn, None, None, None, false);
assert!(res.is_err(), "no-filter forget must error");
assert!(
res.unwrap_err()
.to_string()
.contains("at least one of namespace, pattern, or tier")
);
}
#[test]
fn requires_global_confirmation_pattern_no_namespace() {
let mut a = args();
a.pattern = Some("apple".into());
assert!(requires_global_confirmation(&a));
}
#[test]
fn requires_global_confirmation_tier_no_namespace() {
let mut a = args();
a.tier = Some(Tier::Long.as_str().into());
assert!(requires_global_confirmation(&a));
}
#[test]
fn does_not_require_confirmation_when_namespace_present() {
let mut a = args();
a.namespace = Some("ns".into());
a.pattern = Some("apple".into());
assert!(!requires_global_confirmation(&a));
}
#[test]
fn does_not_require_confirmation_when_only_namespace_set() {
let mut a = args();
a.namespace = Some("ns".into());
assert!(!requires_global_confirmation(&a));
}
#[test]
fn does_not_require_confirmation_when_confirm_flag_set() {
let mut a = args();
a.pattern = Some("apple".into());
a.confirm_global = true;
assert!(!requires_global_confirmation(&a));
}
#[test]
fn cmd_forget_refuses_global_pattern_without_confirm() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "ns", "apple pie", "yum");
let mut a = args();
a.pattern = Some("apple".into());
let mut out = env.output();
let res = cmd_forget(&db, &a, true, &mut out);
assert!(res.is_err(), "expected refusal");
let msg = res.unwrap_err().to_string();
assert!(msg.contains("--confirm-global"), "got: {msg}");
}
#[test]
fn cmd_forget_proceeds_with_confirm_global() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "ns", "apple pie", "yum");
let _ = seed_memory(&db, "other", "apple cake", "yum");
let mut a = args();
a.pattern = Some("apple".into());
a.confirm_global = true;
{
let mut out = env.output();
cmd_forget(&db, &a, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["deleted"].as_u64().unwrap(), 2);
}
#[test]
fn test_forget_text_output_count() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let _ = seed_memory(&db, "ns", "a", "x");
let _ = seed_memory(&db, "ns", "b", "y");
let mut a = args();
a.namespace = Some("ns".to_string());
{
let mut out = env.output();
cmd_forget(&db, &a, false, &mut out).unwrap();
}
let stdout = env.stdout_str();
assert!(stdout.contains("forgot 2 memories"), "got: {stdout}");
}
}