1use crate::cli::CliOutput;
7use crate::cli::helpers::{human_age, id_short};
8use crate::models::Tier;
9use crate::{db, validate};
10use anyhow::Result;
11use clap::Args;
12use std::path::Path;
13
14#[derive(Args)]
17pub struct SearchArgs {
18 #[arg(allow_hyphen_values = true)]
19 pub query: String,
20 #[arg(long, short)]
21 pub namespace: Option<String>,
22 #[arg(long, short)]
23 pub tier: Option<String>,
24 #[arg(long, default_value_t = 20)]
25 pub limit: usize,
26 #[arg(long)]
27 pub since: Option<String>,
28 #[arg(long)]
29 pub until: Option<String>,
30 #[arg(long)]
31 pub tags: Option<String>,
32 #[arg(long)]
34 pub agent_id: Option<String>,
35 #[arg(long)]
38 pub as_agent: Option<String>,
39 #[arg(long)]
43 pub include_archived: bool,
44}
45
46pub fn run(
50 db_path: &Path,
51 args: &SearchArgs,
52 json_out: bool,
53 out: &mut CliOutput<'_>,
54) -> Result<()> {
55 if let Some(ref aid) = args.agent_id {
57 validate::validate_agent_id(aid)?;
58 }
59 if let Some(ref a) = args.as_agent {
61 validate::validate_namespace(a)?;
62 }
63 let conn = db::open(db_path)?;
64 let tier = args.tier.as_deref().and_then(Tier::from_str);
65 let results = db::search(
66 &conn,
67 &args.query,
68 args.namespace.as_deref(),
69 tier.as_ref(),
70 args.limit,
71 None,
72 args.since.as_deref(),
73 args.until.as_deref(),
74 args.tags.as_deref(),
75 args.agent_id.as_deref(),
76 args.as_agent.as_deref(),
77 args.include_archived,
78 )?;
79 if json_out {
80 writeln!(
81 out.stdout,
82 "{}",
83 serde_json::to_string(
84 &serde_json::json!({"results": results, "count": results.len()})
85 )?
86 )?;
87 return Ok(());
88 }
89 if results.is_empty() {
90 writeln!(out.stderr, "no results for: {}", args.query)?;
91 return Ok(());
92 }
93 for mem in &results {
94 let age = human_age(&mem.updated_at);
95 writeln!(
96 out.stdout,
97 "[{}/{}] {} (p={}, ns={}, {})",
98 mem.tier,
99 id_short(&mem.id),
100 mem.title,
101 mem.priority,
102 mem.namespace,
103 age
104 )?;
105 }
106 writeln!(out.stdout, "\n{} result(s)", results.len())?;
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::cli::test_utils::{TestEnv, seed_memory};
114
115 fn default_args() -> SearchArgs {
116 SearchArgs {
117 query: "needle".to_string(),
118 namespace: None,
119 tier: None,
120 limit: 20,
121 since: None,
122 until: None,
123 tags: None,
124 agent_id: None,
125 as_agent: None,
126 include_archived: false,
127 }
128 }
129
130 #[test]
131 fn test_search_happy_path_text() {
132 let mut env = TestEnv::fresh();
133 let db = env.db_path.clone();
134 seed_memory(&db, "test", "needle title", "haystack content");
135 let args = default_args();
136 {
137 let mut out = env.output();
138 run(&db, &args, false, &mut out).unwrap();
139 }
140 let stdout = env.stdout_str();
141 assert!(stdout.contains("needle title"), "got: {stdout}");
142 assert!(stdout.contains("result(s)"));
143 }
144
145 #[test]
146 fn test_search_happy_path_json() {
147 let mut env = TestEnv::fresh();
148 let db = env.db_path.clone();
149 seed_memory(&db, "test", "needle title", "haystack content");
150 let args = default_args();
151 {
152 let mut out = env.output();
153 run(&db, &args, true, &mut out).unwrap();
154 }
155 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
156 assert!(v["count"].as_u64().unwrap() >= 1);
157 assert!(v["results"].is_array());
158 }
159
160 #[test]
161 fn test_search_no_results() {
162 let mut env = TestEnv::fresh();
163 let db = env.db_path.clone();
164 let args = default_args();
165 {
166 let mut out = env.output();
167 run(&db, &args, false, &mut out).unwrap();
168 }
169 assert_eq!(env.stdout_str(), "");
171 assert!(
172 env.stderr_str().contains("no results for: needle"),
173 "got: {}",
174 env.stderr_str()
175 );
176 }
177
178 #[test]
179 fn test_search_with_namespace_filter() {
180 let mut env = TestEnv::fresh();
181 let db = env.db_path.clone();
182 seed_memory(&db, "ns-a", "needle in a", "content a");
183 seed_memory(&db, "ns-b", "needle in b", "content b");
184 let mut args = default_args();
185 args.namespace = Some("ns-a".to_string());
186 {
187 let mut out = env.output();
188 run(&db, &args, true, &mut out).unwrap();
189 }
190 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
191 let results = v["results"].as_array().unwrap();
192 for r in results {
193 assert_eq!(r["namespace"].as_str().unwrap(), "ns-a");
194 }
195 }
196
197 #[test]
198 fn test_search_with_tier_filter() {
199 let mut env = TestEnv::fresh();
201 let db = env.db_path.clone();
202 seed_memory(&db, "test", "needle title", "content");
203 let mut args = default_args();
204 args.tier = Some(Tier::Long.as_str().to_string());
205 {
206 let mut out = env.output();
207 run(&db, &args, true, &mut out).unwrap();
208 }
209 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
210 assert_eq!(v["count"].as_u64().unwrap(), 0);
211 }
212
213 #[test]
214 fn test_search_with_agent_id_filter() {
215 let mut env = TestEnv::fresh();
218 let db = env.db_path.clone();
219 seed_memory(&db, "test", "needle title", "content");
220 let mut args = default_args();
221 args.agent_id = Some("other-agent".to_string());
222 {
223 let mut out = env.output();
224 run(&db, &args, true, &mut out).unwrap();
225 }
226 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
227 assert_eq!(v["count"].as_u64().unwrap(), 0);
228
229 let mut env2 = TestEnv::fresh();
231 let db2 = env2.db_path.clone();
232 seed_memory(&db2, "test", "needle title", "content");
233 let mut args2 = default_args();
234 args2.agent_id = Some("test-agent".to_string());
235 {
236 let mut out = env2.output();
237 run(&db2, &args2, true, &mut out).unwrap();
238 }
239 let v2: serde_json::Value = serde_json::from_str(env2.stdout_str().trim()).unwrap();
240 assert!(v2["count"].as_u64().unwrap() >= 1);
241 }
242
243 #[test]
244 fn test_search_invalid_agent_id_validation_error() {
245 let mut env = TestEnv::fresh();
246 let db = env.db_path.clone();
247 let mut args = default_args();
248 args.agent_id = Some(String::new());
250 let mut out = env.output();
251 let res = run(&db, &args, false, &mut out);
252 assert!(res.is_err(), "expected validate_agent_id to reject empty");
253 }
254
255 #[test]
256 fn test_search_invalid_as_agent_namespace_validation_error() {
257 let mut env = TestEnv::fresh();
258 let db = env.db_path.clone();
259 let mut args = default_args();
260 args.as_agent = Some(String::new());
261 let mut out = env.output();
262 let res = run(&db, &args, false, &mut out);
263 assert!(res.is_err(), "expected validate_namespace to reject empty");
264 }
265}