use crate::models::field_names;
use anyhow::Result;
use clap::Args;
use serde_json::{Value, json};
use crate::cli::CliOutput;
use crate::storage as db;
#[derive(Args, Debug, Clone)]
pub struct KgInvalidateArgs {
#[arg(long = "source-id", value_name = "ID")]
pub source_id: String,
#[arg(long = "target-id", value_name = "ID")]
pub target_id: String,
#[arg(long, value_name = "REL")]
pub relation: String,
#[arg(long = "valid-until", value_name = "RFC3339")]
pub valid_until: Option<String>,
#[arg(long = "agent-id", value_name = "AGENT_ID")]
pub agent_id: Option<String>,
#[arg(long)]
pub json: bool,
}
pub fn cmd_kg_invalidate(
db_path: &std::path::Path,
args: &KgInvalidateArgs,
out: &mut CliOutput<'_>,
) -> Result<()> {
let conn = db::open(db_path)?;
let mut params = json!({
"source_id": args.source_id,
"target_id": args.target_id,
"relation": args.relation,
});
if let Some(t) = &args.valid_until {
params[field_names::VALID_UNTIL] = json!(t);
}
if let Some(a) = &args.agent_id {
params["agent_id"] = json!(a);
}
let envelope = crate::mcp::handle_kg_invalidate(&conn, db_path, ¶ms)
.map_err(|e| anyhow::anyhow!("kg-invalidate: {e}"))?;
if args.json {
writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
return Ok(());
}
let found = envelope
.get("found")
.and_then(Value::as_bool)
.unwrap_or(false);
if found {
let vu = envelope
.get(field_names::VALID_UNTIL)
.and_then(Value::as_str)
.unwrap_or("?");
writeln!(out.stdout, "kg-invalidate: invalidated valid_until={vu}")?;
} else {
writeln!(out.stdout, "kg-invalidate: not found")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
#[test]
fn kg_invalidate_cli_nonexistent_returns_not_found() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let a = seed_memory(&db, "ns", "src", "alpha");
let b = seed_memory(&db, "ns", "tgt", "beta");
let args = KgInvalidateArgs {
source_id: a,
target_id: b,
relation: "related_to".into(),
valid_until: None,
agent_id: None,
json: true,
};
{
let mut out = env.output();
cmd_kg_invalidate(&db, &args, &mut out).expect("ok");
}
let stdout = env.stdout_str();
let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
assert_eq!(envelope["found"].as_bool(), Some(false));
}
#[test]
fn kg_invalidate_cli_invalid_triple_returns_err() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = KgInvalidateArgs {
source_id: "bogus id with spaces".into(),
target_id: "another".into(),
relation: "related_to".into(),
valid_until: None,
agent_id: None,
json: true,
};
let mut out = env.output();
let err = cmd_kg_invalidate(&db, &args, &mut out).expect_err("must fail");
assert!(err.to_string().contains("kg-invalidate"), "got: {err}");
}
#[test]
fn kg_invalidate_cli_text_output_found_with_params() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let a = seed_memory(&db, "ns", "src", "alpha");
let b = seed_memory(&db, "ns", "tgt", "beta");
{
let conn = db::open(&db).unwrap();
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO memory_links (source_id, target_id, relation, created_at, valid_from)
VALUES (?1, ?2, 'related_to', ?3, ?3)",
rusqlite::params![a, b, now],
)
.expect("insert link");
}
let args = KgInvalidateArgs {
source_id: a,
target_id: b,
relation: "related_to".into(),
valid_until: Some("2026-06-01T00:00:00+00:00".into()),
agent_id: Some("ai:invalidator".into()),
json: false,
};
{
let mut out = env.output();
cmd_kg_invalidate(&db, &args, &mut out).expect("ok");
}
let stdout = env.stdout_str();
assert!(stdout.contains("invalidated"), "got: {stdout}");
assert!(stdout.contains("valid_until="), "got: {stdout}");
}
#[test]
fn kg_invalidate_cli_text_output_not_found() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let a = seed_memory(&db, "ns", "src2", "alpha");
let b = seed_memory(&db, "ns", "tgt2", "beta");
let args = KgInvalidateArgs {
source_id: a,
target_id: b,
relation: "related_to".into(),
valid_until: None,
agent_id: None,
json: false,
};
{
let mut out = env.output();
cmd_kg_invalidate(&db, &args, &mut out).expect("ok");
}
assert!(env.stdout_str().contains("kg-invalidate: not found"));
}
}