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