1use crate::cli::CliOutput;
7use crate::{db, validate};
8use anyhow::Result;
9use clap::Args;
10use std::path::Path;
11
12#[derive(Args)]
13pub struct UpdateArgs {
14 pub id: String,
15 #[arg(long, short = 'T', allow_hyphen_values = true)]
16 pub title: Option<String>,
17 #[arg(long, short, allow_hyphen_values = true)]
18 pub content: Option<String>,
19 #[arg(long, short)]
20 pub tier: Option<String>,
21 #[arg(long, short)]
22 pub namespace: Option<String>,
23 #[arg(long)]
24 pub tags: Option<String>,
25 #[arg(long, short)]
26 pub priority: Option<i32>,
27 #[arg(long)]
28 pub confidence: Option<f64>,
29 #[arg(long)]
31 pub expires_at: Option<String>,
32}
33
34pub fn run(
36 db_path: &Path,
37 args: &UpdateArgs,
38 json_out: bool,
39 out: &mut CliOutput<'_>,
40) -> Result<()> {
41 use crate::models::Tier;
42 validate::validate_id(&args.id)?;
43 let conn = db::open(db_path)?;
44 let resolved_id = if db::get(&conn, &args.id)?.is_some() {
45 args.id.clone()
46 } else if let Some(mem) = db::get_by_prefix(&conn, &args.id)? {
47 mem.id
48 } else {
49 writeln!(out.stderr, "not found: {}", args.id)?;
50 std::process::exit(1);
51 };
52 let tier = args.tier.as_deref().and_then(Tier::from_str);
53 let tags: Option<Vec<String>> = args.tags.as_ref().map(|t| {
54 t.split(',')
55 .map(|s| s.trim().to_string())
56 .filter(|s| !s.is_empty())
57 .collect()
58 });
59 if let Some(ref t) = args.title {
60 validate::validate_title(t)?;
61 }
62 if let Some(ref c) = args.content {
63 validate::validate_content(c)?;
64 }
65 if let Some(ref ns) = args.namespace {
66 validate::validate_namespace(ns)?;
67 }
68 if let Some(ref tags) = tags {
69 validate::validate_tags(tags)?;
70 }
71 if let Some(p) = args.priority {
72 validate::validate_priority(p)?;
73 }
74 if let Some(c) = args.confidence {
75 validate::validate_confidence(c)?;
76 }
77 if let Some(ref ts) = args.expires_at
78 && !ts.is_empty()
79 {
80 validate::validate_expires_at_format(ts)?;
81 }
82 let (found, _content_changed) = db::update(
83 &conn,
84 &resolved_id,
85 args.title.as_deref(),
86 args.content.as_deref(),
87 tier.as_ref(),
88 args.namespace.as_deref(),
89 tags.as_ref(),
90 args.priority,
91 args.confidence,
92 args.expires_at.as_deref(),
93 None,
94 )?;
95 if !found {
96 writeln!(out.stderr, "not found: {}", args.id)?;
97 std::process::exit(1);
98 }
99 if let Some(mem) = db::get(&conn, &resolved_id)? {
100 crate::audit::emit(crate::audit::EventBuilder::new(
102 crate::audit::AuditAction::Update,
103 crate::audit::actor(
104 mem.metadata
105 .get("agent_id")
106 .and_then(|v| v.as_str())
107 .unwrap_or_default(),
108 "default_fallback",
109 None,
110 ),
111 crate::audit::target_memory(
112 mem.id.clone(),
113 mem.namespace.clone(),
114 Some(mem.title.clone()),
115 Some(mem.tier.to_string()),
116 None,
117 ),
118 ));
119 if json_out {
120 writeln!(out.stdout, "{}", serde_json::to_string(&mem)?)?;
121 } else {
122 writeln!(out.stdout, "updated: {} [{}]", mem.id, mem.title)?;
123 }
124 }
125 Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::cli::test_utils::{TestEnv, seed_memory};
132
133 fn empty_args(id: &str) -> UpdateArgs {
134 UpdateArgs {
135 id: id.to_string(),
136 title: None,
137 content: None,
138 tier: None,
139 namespace: None,
140 tags: None,
141 priority: None,
142 confidence: None,
143 expires_at: None,
144 }
145 }
146
147 #[test]
148 fn test_update_happy_path() {
149 let mut env = TestEnv::fresh();
150 let db = env.db_path.clone();
151 let id = seed_memory(&db, "ns", "old-title", "old content");
152 let mut args = empty_args(&id);
153 args.title = Some("new-title".to_string());
154 args.content = Some("new content".to_string());
155 {
156 let mut out = env.output();
157 run(&db, &args, false, &mut out).unwrap();
158 }
159 assert!(env.stdout_str().contains("updated:"));
160 assert!(env.stdout_str().contains("new-title"));
161 }
162
163 #[test]
164 fn test_update_by_prefix_id() {
165 let mut env = TestEnv::fresh();
166 let db = env.db_path.clone();
167 let id = seed_memory(&db, "ns", "title-a", "content-a");
168 let prefix = &id[..8];
170 let mut args = empty_args(prefix);
171 args.title = Some("renamed".to_string());
172 {
173 let mut out = env.output();
174 run(&db, &args, false, &mut out).unwrap();
175 }
176 assert!(env.stdout_str().contains("renamed"));
177 }
178
179 #[test]
184 fn test_update_partial_only_title() {
185 let mut env = TestEnv::fresh();
186 let db = env.db_path.clone();
187 let id = seed_memory(&db, "ns", "orig-title", "orig content");
188 let mut args = empty_args(&id);
189 args.title = Some("title-only-change".to_string());
190 {
191 let mut out = env.output();
192 run(&db, &args, true, &mut out).unwrap();
193 }
194 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
195 assert_eq!(v["title"].as_str().unwrap(), "title-only-change");
196 assert_eq!(v["content"].as_str().unwrap(), "orig content");
197 }
198
199 #[test]
200 fn test_update_partial_only_content() {
201 let mut env = TestEnv::fresh();
202 let db = env.db_path.clone();
203 let id = seed_memory(&db, "ns", "kept-title", "old-content");
204 let mut args = empty_args(&id);
205 args.content = Some("new content body".to_string());
206 {
207 let mut out = env.output();
208 run(&db, &args, true, &mut out).unwrap();
209 }
210 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
211 assert_eq!(v["title"].as_str().unwrap(), "kept-title");
212 assert_eq!(v["content"].as_str().unwrap(), "new content body");
213 }
214
215 #[test]
216 fn test_update_clear_expires_at_with_empty_string() {
217 let mut env = TestEnv::fresh();
218 let db = env.db_path.clone();
219 let id = seed_memory(&db, "ns", "tt", "cc");
220 let mut args = empty_args(&id);
221 args.expires_at = Some(String::new());
222 {
223 let mut out = env.output();
224 run(&db, &args, false, &mut out).unwrap();
227 }
228 assert!(env.stdout_str().contains("updated:"));
229 }
230
231 #[test]
232 fn test_update_invalid_priority_validation_error() {
233 let mut env = TestEnv::fresh();
234 let db = env.db_path.clone();
235 let id = seed_memory(&db, "ns", "tt", "cc");
236 let mut args = empty_args(&id);
237 args.priority = Some(99);
238 let mut out = env.output();
239 let res = run(&db, &args, false, &mut out);
240 assert!(res.is_err());
241 }
242}