1use anyhow::Context as _;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use tokio::sync::RwLock;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct RelevanceConfig {
20 #[serde(default = "RelevanceConfig::default_decay_days")]
22 pub decay_days: f32,
23 #[serde(default = "RelevanceConfig::default_importance_weight")]
25 pub importance_weight: f32,
26 #[serde(default = "RelevanceConfig::default_recency_weight")]
28 pub recency_weight: f32,
29}
30
31impl RelevanceConfig {
32 fn default_decay_days() -> f32 {
33 30.0
34 }
35 fn default_importance_weight() -> f32 {
36 0.7
37 }
38 fn default_recency_weight() -> f32 {
39 0.3
40 }
41}
42
43impl Default for RelevanceConfig {
44 fn default() -> Self {
45 Self {
46 decay_days: 30.0,
47 importance_weight: 0.7,
48 recency_weight: 0.3,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct MemoryItem {
60 pub id: String,
61 pub content: String,
62 pub timestamp: DateTime<Utc>,
63 pub importance: f32,
64 pub tags: Vec<String>,
65 pub memory_type: MemoryType,
66 pub metadata: HashMap<String, String>,
67 pub access_count: u32,
68 pub last_accessed: Option<DateTime<Utc>>,
69 #[serde(skip)]
70 pub content_lower: String,
71}
72
73impl MemoryItem {
74 pub fn new(content: impl Into<String>) -> Self {
75 let content = content.into();
76 let content_lower = content.to_lowercase();
77 Self {
78 id: uuid::Uuid::new_v4().to_string(),
79 content,
80 timestamp: Utc::now(),
81 importance: 0.5,
82 tags: Vec::new(),
83 memory_type: MemoryType::Episodic,
84 metadata: HashMap::new(),
85 access_count: 0,
86 last_accessed: None,
87 content_lower,
88 }
89 }
90
91 pub fn with_importance(mut self, importance: f32) -> Self {
92 self.importance = importance.clamp(0.0, 1.0);
93 self
94 }
95
96 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
97 self.tags = tags;
98 self
99 }
100
101 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
102 self.tags.push(tag.into());
103 self
104 }
105
106 pub fn with_type(mut self, memory_type: MemoryType) -> Self {
107 self.memory_type = memory_type;
108 self
109 }
110
111 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
112 self.metadata.insert(key.into(), value.into());
113 self
114 }
115
116 pub fn record_access(&mut self) {
117 self.access_count += 1;
118 self.last_accessed = Some(Utc::now());
119 }
120
121 pub fn relevance_score_at(&self, now: DateTime<Utc>, config: &RelevanceConfig) -> f32 {
123 let age_days = (now - self.timestamp).num_seconds() as f32 / 86400.0;
124 let decay = (-age_days / config.decay_days).exp();
125 self.importance * config.importance_weight + decay * config.recency_weight
126 }
127
128 pub fn relevance_score(&self) -> f32 {
130 self.relevance_score_at(Utc::now(), &RelevanceConfig::default())
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum MemoryType {
138 Episodic,
139 Semantic,
140 Procedural,
141 Working,
142}
143
144#[async_trait::async_trait]
149pub trait MemoryStore: Send + Sync {
150 async fn store(&self, item: MemoryItem) -> anyhow::Result<()>;
151 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>>;
152 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
153 async fn search_by_tags(
154 &self,
155 tags: &[String],
156 limit: usize,
157 ) -> anyhow::Result<Vec<MemoryItem>>;
158 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
159 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
160 async fn delete(&self, id: &str) -> anyhow::Result<()>;
161 async fn clear(&self) -> anyhow::Result<()>;
162 async fn count(&self) -> anyhow::Result<usize>;
163}
164
165fn index_score(entry: &IndexEntry, now: DateTime<Utc>, config: &RelevanceConfig) -> f32 {
171 let age_days = (now - entry.timestamp).num_seconds() as f32 / 86400.0;
172 let decay = (-age_days / config.decay_days).exp();
173 entry.importance * config.importance_weight + decay * config.recency_weight
174}
175
176fn sort_by_relevance(items: &mut [MemoryItem]) {
177 let now = Utc::now();
178 let config = RelevanceConfig::default();
179 items.sort_by(|a, b| {
180 b.relevance_score_at(now, &config)
181 .partial_cmp(&a.relevance_score_at(now, &config))
182 .unwrap_or(std::cmp::Ordering::Equal)
183 });
184}
185
186pub struct InMemoryStore {
194 items: RwLock<Vec<MemoryItem>>,
195}
196
197impl Default for InMemoryStore {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203impl InMemoryStore {
204 pub fn new() -> Self {
205 Self {
206 items: RwLock::new(Vec::new()),
207 }
208 }
209}
210
211#[async_trait::async_trait]
212impl MemoryStore for InMemoryStore {
213 async fn store(&self, item: MemoryItem) -> anyhow::Result<()> {
214 let mut items = self.items.write().await;
215 if let Some(pos) = items.iter().position(|i| i.id == item.id) {
216 items[pos] = item;
217 } else {
218 items.push(item);
219 }
220 Ok(())
221 }
222
223 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
224 Ok(self.items.read().await.iter().find(|i| i.id == id).cloned())
225 }
226
227 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
228 let query_lower = query.to_lowercase();
229 let config = RelevanceConfig::default();
230 let now = Utc::now();
231 let items = self.items.read().await;
232 let mut matches: Vec<MemoryItem> = items
233 .iter()
234 .filter(|i| i.content_lower.contains(&query_lower))
235 .cloned()
236 .collect();
237 matches.sort_by(|a, b| {
238 b.relevance_score_at(now, &config)
239 .partial_cmp(&a.relevance_score_at(now, &config))
240 .unwrap_or(std::cmp::Ordering::Equal)
241 });
242 matches.truncate(limit);
243 Ok(matches)
244 }
245
246 async fn search_by_tags(
247 &self,
248 tags: &[String],
249 limit: usize,
250 ) -> anyhow::Result<Vec<MemoryItem>> {
251 let config = RelevanceConfig::default();
252 let now = Utc::now();
253 let items = self.items.read().await;
254 let mut matches: Vec<MemoryItem> = items
255 .iter()
256 .filter(|i| tags.iter().any(|t| i.tags.contains(t)))
257 .cloned()
258 .collect();
259 matches.sort_by(|a, b| {
260 b.relevance_score_at(now, &config)
261 .partial_cmp(&a.relevance_score_at(now, &config))
262 .unwrap_or(std::cmp::Ordering::Equal)
263 });
264 matches.truncate(limit);
265 Ok(matches)
266 }
267
268 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
269 let items = self.items.read().await;
270 let mut sorted: Vec<MemoryItem> = items.iter().cloned().collect();
271 sorted.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
272 sorted.truncate(limit);
273 Ok(sorted)
274 }
275
276 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
277 let items = self.items.read().await;
278 let mut matches: Vec<MemoryItem> = items
279 .iter()
280 .filter(|i| i.importance >= threshold)
281 .cloned()
282 .collect();
283 matches.sort_by(|a, b| {
284 b.importance
285 .partial_cmp(&a.importance)
286 .unwrap_or(std::cmp::Ordering::Equal)
287 });
288 matches.truncate(limit);
289 Ok(matches)
290 }
291
292 async fn delete(&self, id: &str) -> anyhow::Result<()> {
293 self.items.write().await.retain(|i| i.id != id);
294 Ok(())
295 }
296
297 async fn clear(&self) -> anyhow::Result<()> {
298 self.items.write().await.clear();
299 Ok(())
300 }
301
302 async fn count(&self) -> anyhow::Result<usize> {
303 Ok(self.items.read().await.len())
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
312struct IndexEntry {
313 id: String,
314 content_lower: String,
315 tags: Vec<String>,
316 importance: f32,
317 timestamp: DateTime<Utc>,
318 memory_type: MemoryType,
319}
320
321impl From<&MemoryItem> for IndexEntry {
322 fn from(item: &MemoryItem) -> Self {
323 Self {
324 id: item.id.clone(),
325 content_lower: item.content.to_lowercase(),
326 tags: item.tags.clone(),
327 importance: item.importance,
328 timestamp: item.timestamp,
329 memory_type: item.memory_type,
330 }
331 }
332}
333
334pub struct FileMemoryStore {
342 items_dir: std::path::PathBuf,
343 index_path: std::path::PathBuf,
344 index: RwLock<Vec<IndexEntry>>,
345}
346
347impl FileMemoryStore {
348 pub async fn new(dir: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
349 let dir = dir.as_ref().to_path_buf();
350 let items_dir = dir.join("items");
351 let index_path = dir.join("index.json");
352
353 tokio::fs::create_dir_all(&items_dir)
354 .await
355 .with_context(|| {
356 format!("Failed to create memory directory: {}", items_dir.display())
357 })?;
358
359 let index = if index_path.exists() {
360 let data = tokio::fs::read_to_string(&index_path)
361 .await
362 .with_context(|| {
363 format!("Failed to read memory index: {}", index_path.display())
364 })?;
365 serde_json::from_str(&data).unwrap_or_default()
366 } else {
367 Vec::new()
368 };
369
370 Ok(Self {
371 items_dir,
372 index_path,
373 index: RwLock::new(index),
374 })
375 }
376
377 fn safe_id(id: &str) -> String {
378 id.replace(['/', '\\'], "_").replace("..", "_")
379 }
380
381 fn item_path(&self, id: &str) -> std::path::PathBuf {
382 self.items_dir.join(format!("{}.json", Self::safe_id(id)))
383 }
384
385 async fn save_index(&self) -> anyhow::Result<()> {
386 let index = self.index.read().await;
387 let json = serde_json::to_string(&*index).context("Failed to serialize memory index")?;
388 drop(index);
389 let tmp = self.index_path.with_extension("json.tmp");
390 tokio::fs::write(&tmp, json.as_bytes())
391 .await
392 .context("Failed to write memory index temp file")?;
393 tokio::fs::rename(&tmp, &self.index_path)
394 .await
395 .context("Failed to rename memory index")?;
396 Ok(())
397 }
398
399 async fn save_item(&self, item: &MemoryItem) -> anyhow::Result<()> {
400 let path = self.item_path(&item.id);
401 let json = serde_json::to_string_pretty(item)
402 .with_context(|| format!("Failed to serialize memory item: {}", item.id))?;
403 let tmp = path.with_extension("json.tmp");
404 tokio::fs::write(&tmp, json.as_bytes())
405 .await
406 .with_context(|| format!("Failed to write memory item: {}", item.id))?;
407 tokio::fs::rename(&tmp, &path)
408 .await
409 .with_context(|| format!("Failed to rename memory item: {}", item.id))?;
410 Ok(())
411 }
412
413 pub async fn rebuild_index(&self) -> anyhow::Result<usize> {
415 let mut entries = tokio::fs::read_dir(&self.items_dir).await?;
416 let mut new_index = Vec::new();
417 while let Some(entry) = entries.next_entry().await? {
418 let path = entry.path();
419 if path.extension().is_some_and(|ext| ext == "json") {
420 if let Ok(data) = tokio::fs::read_to_string(&path).await {
421 if let Ok(item) = serde_json::from_str::<MemoryItem>(&data) {
422 new_index.push(IndexEntry::from(&item));
423 }
424 }
425 }
426 }
427 let count = new_index.len();
428 *self.index.write().await = new_index;
429 self.save_index().await?;
430 Ok(count)
431 }
432}
433
434#[async_trait::async_trait]
435impl MemoryStore for FileMemoryStore {
436 async fn store(&self, item: MemoryItem) -> anyhow::Result<()> {
437 let mut item = item;
438 item.id = Self::safe_id(&item.id);
439 self.save_item(&item).await?;
440 let entry = IndexEntry::from(&item);
441 let mut index = self.index.write().await;
442 if let Some(pos) = index.iter().position(|e| e.id == item.id) {
443 index[pos] = entry;
444 } else {
445 index.push(entry);
446 }
447 drop(index);
448 self.save_index().await
449 }
450
451 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
452 let path = self.item_path(id);
453 if !path.exists() {
454 return Ok(None);
455 }
456 let data = tokio::fs::read_to_string(&path).await?;
457 let mut item: MemoryItem = serde_json::from_str(&data)?;
458 item.content_lower = item.content.to_lowercase();
459 Ok(Some(item))
460 }
461
462 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
463 let query_lower = query.to_lowercase();
464 let index = self.index.read().await;
465 let now = Utc::now();
466 let config = RelevanceConfig::default();
467 let mut matches: Vec<&IndexEntry> = index
468 .iter()
469 .filter(|e| e.content_lower.contains(&query_lower))
470 .collect();
471 matches.sort_by(|a, b| {
472 index_score(a, now, &config)
473 .partial_cmp(&index_score(b, now, &config))
474 .unwrap_or(std::cmp::Ordering::Equal)
475 .reverse()
476 });
477 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
478 drop(index);
479 let mut items = Vec::with_capacity(ids.len());
480 for id in ids {
481 if let Some(item) = self.retrieve(&id).await? {
482 items.push(item);
483 }
484 }
485 sort_by_relevance(&mut items);
486 Ok(items)
487 }
488
489 async fn search_by_tags(
490 &self,
491 tags: &[String],
492 limit: usize,
493 ) -> anyhow::Result<Vec<MemoryItem>> {
494 let index = self.index.read().await;
495 let now = Utc::now();
496 let config = RelevanceConfig::default();
497 let mut matches: Vec<&IndexEntry> = index
498 .iter()
499 .filter(|e| tags.iter().any(|t| e.tags.contains(t)))
500 .collect();
501 matches.sort_by(|a, b| {
502 index_score(a, now, &config)
503 .partial_cmp(&index_score(b, now, &config))
504 .unwrap_or(std::cmp::Ordering::Equal)
505 .reverse()
506 });
507 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
508 drop(index);
509 let mut items = Vec::with_capacity(ids.len());
510 for id in ids {
511 if let Some(item) = self.retrieve(&id).await? {
512 items.push(item);
513 }
514 }
515 sort_by_relevance(&mut items);
516 Ok(items)
517 }
518
519 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
520 let index = self.index.read().await;
521 let mut sorted: Vec<&IndexEntry> = index.iter().collect();
522 sorted.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
523 let ids: Vec<String> = sorted.iter().take(limit).map(|e| e.id.clone()).collect();
524 drop(index);
525 let mut items = Vec::with_capacity(ids.len());
526 for id in ids {
527 if let Some(item) = self.retrieve(&id).await? {
528 items.push(item);
529 }
530 }
531 items.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
532 Ok(items)
533 }
534
535 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
536 let index = self.index.read().await;
537 let mut matches: Vec<&IndexEntry> =
538 index.iter().filter(|e| e.importance >= threshold).collect();
539 matches.sort_by(|a, b| {
540 b.importance
541 .partial_cmp(&a.importance)
542 .unwrap_or(std::cmp::Ordering::Equal)
543 });
544 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
545 drop(index);
546 let mut items = Vec::with_capacity(ids.len());
547 for id in ids {
548 if let Some(item) = self.retrieve(&id).await? {
549 items.push(item);
550 }
551 }
552 items.sort_by(|a, b| {
553 b.importance
554 .partial_cmp(&a.importance)
555 .unwrap_or(std::cmp::Ordering::Equal)
556 });
557 Ok(items)
558 }
559
560 async fn delete(&self, id: &str) -> anyhow::Result<()> {
561 let path = self.item_path(id);
562 if path.exists() {
563 tokio::fs::remove_file(&path).await?;
564 }
565 let mut index = self.index.write().await;
566 index.retain(|e| e.id != id);
567 drop(index);
568 self.save_index().await
569 }
570
571 async fn clear(&self) -> anyhow::Result<()> {
572 let mut entries = tokio::fs::read_dir(&self.items_dir).await?;
573 while let Some(entry) = entries.next_entry().await? {
574 let path = entry.path();
575 if path.extension().is_some_and(|ext| ext == "json") {
576 let _ = tokio::fs::remove_file(&path).await;
577 }
578 }
579 self.index.write().await.clear();
580 self.save_index().await
581 }
582
583 async fn count(&self) -> anyhow::Result<usize> {
584 Ok(self.index.read().await.len())
585 }
586}
587
588#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
599 fn test_memory_item_creation() {
600 let item = MemoryItem::new("Test memory")
601 .with_importance(0.8)
602 .with_tag("test")
603 .with_type(MemoryType::Semantic);
604 assert_eq!(item.content, "Test memory");
605 assert_eq!(item.importance, 0.8);
606 assert_eq!(item.tags, vec!["test"]);
607 assert_eq!(item.memory_type, MemoryType::Semantic);
608 }
609
610 #[test]
611 fn test_memory_item_importance_clamped() {
612 assert_eq!(MemoryItem::new("x").with_importance(1.5).importance, 1.0);
613 assert_eq!(MemoryItem::new("x").with_importance(-0.5).importance, 0.0);
614 }
615
616 #[test]
617 fn test_memory_item_record_access() {
618 let mut item = MemoryItem::new("test");
619 assert_eq!(item.access_count, 0);
620 item.record_access();
621 assert_eq!(item.access_count, 1);
622 assert!(item.last_accessed.is_some());
623 }
624
625 #[test]
626 fn test_memory_item_default_type_is_episodic() {
627 assert_eq!(MemoryItem::new("test").memory_type, MemoryType::Episodic);
628 }
629
630 #[test]
631 fn test_memory_item_all_types() {
632 assert_eq!(
633 MemoryItem::new("e")
634 .with_type(MemoryType::Episodic)
635 .memory_type,
636 MemoryType::Episodic
637 );
638 assert_eq!(
639 MemoryItem::new("s")
640 .with_type(MemoryType::Semantic)
641 .memory_type,
642 MemoryType::Semantic
643 );
644 assert_eq!(
645 MemoryItem::new("p")
646 .with_type(MemoryType::Procedural)
647 .memory_type,
648 MemoryType::Procedural
649 );
650 assert_eq!(
651 MemoryItem::new("w")
652 .with_type(MemoryType::Working)
653 .memory_type,
654 MemoryType::Working
655 );
656 }
657
658 #[test]
661 fn test_relevance_score_uses_config() {
662 let item = MemoryItem::new("test").with_importance(1.0);
663 let now = Utc::now();
664
665 let config_importance = RelevanceConfig {
667 decay_days: 30.0,
668 importance_weight: 0.9,
669 recency_weight: 0.1,
670 };
671 let score = item.relevance_score_at(now, &config_importance);
672 assert!(score > 0.95, "score was {score}");
673
674 let config_fast_decay = RelevanceConfig {
676 decay_days: 1.0,
677 importance_weight: 0.7,
678 recency_weight: 0.3,
679 };
680 let score2 = item.relevance_score_at(now, &config_fast_decay);
681 assert!(score2 > 0.9, "score was {score2}");
682 }
683
684 #[test]
685 fn test_relevance_score_decays_with_age() {
686 let mut old_item = MemoryItem::new("old").with_importance(0.5);
687 old_item.timestamp = Utc::now() - chrono::Duration::days(60);
688 let config = RelevanceConfig::default(); let score = old_item.relevance_score_at(Utc::now(), &config);
690 assert!(score < 0.45, "score was {score}");
693 }
694
695 #[test]
696 fn test_relevance_score_default_uses_default_config() {
697 let item = MemoryItem::new("test").with_importance(0.9);
698 let score = item.relevance_score();
699 assert!(score > 0.6);
700 }
701
702 #[test]
705 fn test_relevance_config_defaults() {
706 let c = RelevanceConfig::default();
707 assert_eq!(c.decay_days, 30.0);
708 assert_eq!(c.importance_weight, 0.7);
709 assert_eq!(c.recency_weight, 0.3);
710 }
711
712 #[tokio::test]
715 async fn test_in_memory_store_retrieve() {
716 let store = InMemoryStore::new();
717 let item = MemoryItem::new("hello").with_tag("test");
718 store.store(item.clone()).await.unwrap();
719 let r = store.retrieve(&item.id).await.unwrap();
720 assert!(r.is_some());
721 assert_eq!(r.unwrap().content, "hello");
722 }
723
724 #[tokio::test]
725 async fn test_in_memory_store_retrieve_nonexistent() {
726 let store = InMemoryStore::new();
727 assert!(store.retrieve("nope").await.unwrap().is_none());
728 }
729
730 #[tokio::test]
731 async fn test_in_memory_store_upsert() {
732 let store = InMemoryStore::new();
733 let mut item = MemoryItem::new("original");
734 let id = item.id.clone();
735 store.store(item.clone()).await.unwrap();
736 item.content = "updated".to_string();
737 item.content_lower = "updated".to_string();
738 store.store(item).await.unwrap();
739 assert_eq!(store.count().await.unwrap(), 1);
740 assert_eq!(
741 store.retrieve(&id).await.unwrap().unwrap().content,
742 "updated"
743 );
744 }
745
746 #[tokio::test]
747 async fn test_in_memory_store_search_and_tags() {
748 let store = InMemoryStore::new();
749 store
750 .store(MemoryItem::new("create file").with_tag("file"))
751 .await
752 .unwrap();
753 store
754 .store(MemoryItem::new("delete file").with_tag("file"))
755 .await
756 .unwrap();
757 store
758 .store(MemoryItem::new("create dir").with_tag("dir"))
759 .await
760 .unwrap();
761 assert_eq!(store.search("create", 10).await.unwrap().len(), 2);
762 assert_eq!(
763 store
764 .search_by_tags(&["file".to_string()], 10)
765 .await
766 .unwrap()
767 .len(),
768 2
769 );
770 }
771
772 #[tokio::test]
773 async fn test_in_memory_store_search_relevance_order() {
774 let store = InMemoryStore::new();
775 store
776 .store(MemoryItem::new("rust tip").with_importance(0.3))
777 .await
778 .unwrap();
779 store
780 .store(MemoryItem::new("rust trick").with_importance(0.9))
781 .await
782 .unwrap();
783 let results = store.search("rust", 10).await.unwrap();
784 assert_eq!(results.len(), 2);
785 assert!(results[0].importance >= results[1].importance);
786 }
787
788 #[tokio::test]
789 async fn test_in_memory_store_delete_and_clear() {
790 let store = InMemoryStore::new();
791 let item = MemoryItem::new("to delete");
792 let id = item.id.clone();
793 store.store(item).await.unwrap();
794 store.delete(&id).await.unwrap();
795 assert_eq!(store.count().await.unwrap(), 0);
796
797 for i in 0..3 {
798 store
799 .store(MemoryItem::new(format!("item {i}")))
800 .await
801 .unwrap();
802 }
803 store.clear().await.unwrap();
804 assert_eq!(store.count().await.unwrap(), 0);
805 }
806
807 #[tokio::test]
808 async fn test_in_memory_store_get_recent() {
809 let store = InMemoryStore::new();
810 for i in 0..5 {
811 let mut item = MemoryItem::new(format!("item {i}"));
812 item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
813 store.store(item).await.unwrap();
814 }
815 let recent = store.get_recent(3).await.unwrap();
816 assert_eq!(recent.len(), 3);
817 assert!(recent[0].timestamp >= recent[1].timestamp);
818 }
819
820 #[tokio::test]
821 async fn test_in_memory_store_get_important() {
822 let store = InMemoryStore::new();
823 store
824 .store(MemoryItem::new("low").with_importance(0.2))
825 .await
826 .unwrap();
827 store
828 .store(MemoryItem::new("high").with_importance(0.9))
829 .await
830 .unwrap();
831 store
832 .store(MemoryItem::new("medium").with_importance(0.5))
833 .await
834 .unwrap();
835 let results = store.get_important(0.7, 10).await.unwrap();
836 assert_eq!(results.len(), 1);
837 assert_eq!(results[0].content, "high");
838 }
839
840 #[test]
841 fn test_in_memory_store_default() {
842 let _store: InMemoryStore = InMemoryStore::default();
843 }
844}
845
846#[cfg(test)]
847mod file_memory_store_tests {
848 use super::*;
849 use tempfile::TempDir;
850
851 async fn setup() -> (TempDir, FileMemoryStore) {
852 let dir = TempDir::new().unwrap();
853 let store = FileMemoryStore::new(dir.path()).await.unwrap();
854 (dir, store)
855 }
856
857 #[tokio::test]
858 async fn test_store_and_retrieve() {
859 let (_dir, store) = setup().await;
860 let item = MemoryItem::new("hello world");
861 let id = item.id.clone();
862 store.store(item).await.unwrap();
863 let r = store.retrieve(&id).await.unwrap().unwrap();
864 assert_eq!(r.content, "hello world");
865 }
866
867 #[tokio::test]
868 async fn test_retrieve_nonexistent() {
869 let (_dir, store) = setup().await;
870 assert!(store.retrieve("nonexistent").await.unwrap().is_none());
871 }
872
873 #[tokio::test]
874 async fn test_search_by_content() {
875 let (_dir, store) = setup().await;
876 store
877 .store(MemoryItem::new("rust programming"))
878 .await
879 .unwrap();
880 store
881 .store(MemoryItem::new("python scripting"))
882 .await
883 .unwrap();
884 store
885 .store(MemoryItem::new("rust async patterns"))
886 .await
887 .unwrap();
888 let results = store.search("rust", 10).await.unwrap();
889 assert_eq!(results.len(), 2);
890 }
891
892 #[tokio::test]
893 async fn test_search_limit() {
894 let (_dir, store) = setup().await;
895 for i in 0..10 {
896 store
897 .store(MemoryItem::new(format!("item {i}")))
898 .await
899 .unwrap();
900 }
901 assert_eq!(store.search("item", 3).await.unwrap().len(), 3);
902 }
903
904 #[tokio::test]
905 async fn test_search_by_tags() {
906 let (_dir, store) = setup().await;
907 store
908 .store(MemoryItem::new("one").with_tags(vec!["rust".into(), "async".into()]))
909 .await
910 .unwrap();
911 store
912 .store(MemoryItem::new("two").with_tags(vec!["python".into()]))
913 .await
914 .unwrap();
915 store
916 .store(MemoryItem::new("three").with_tags(vec!["rust".into()]))
917 .await
918 .unwrap();
919 assert_eq!(
920 store
921 .search_by_tags(&["rust".to_string()], 10)
922 .await
923 .unwrap()
924 .len(),
925 2
926 );
927 }
928
929 #[tokio::test]
930 async fn test_get_recent_ordered() {
931 let (_dir, store) = setup().await;
932 for i in 0..5 {
933 let mut item = MemoryItem::new(format!("item {i}"));
934 item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
935 store.store(item).await.unwrap();
936 }
937 let results = store.get_recent(3).await.unwrap();
938 assert_eq!(results.len(), 3);
939 assert!(results[0].timestamp >= results[1].timestamp);
940 }
941
942 #[tokio::test]
943 async fn test_get_important() {
944 let (_dir, store) = setup().await;
945 store
946 .store(MemoryItem::new("low").with_importance(0.1))
947 .await
948 .unwrap();
949 store
950 .store(MemoryItem::new("high").with_importance(0.9))
951 .await
952 .unwrap();
953 store
954 .store(MemoryItem::new("medium").with_importance(0.5))
955 .await
956 .unwrap();
957 let results = store.get_important(0.0, 2).await.unwrap();
958 assert_eq!(results.len(), 2);
959 assert!(results[0].importance >= results[1].importance);
960 }
961
962 #[tokio::test]
963 async fn test_delete() {
964 let (_dir, store) = setup().await;
965 let item = MemoryItem::new("to delete");
966 let id = item.id.clone();
967 store.store(item).await.unwrap();
968 store.delete(&id).await.unwrap();
969 assert_eq!(store.count().await.unwrap(), 0);
970 assert!(store.retrieve(&id).await.unwrap().is_none());
971 }
972
973 #[tokio::test]
974 async fn test_delete_nonexistent() {
975 let (_dir, store) = setup().await;
976 store.delete("nonexistent").await.unwrap();
977 }
978
979 #[tokio::test]
980 async fn test_clear() {
981 let (_dir, store) = setup().await;
982 for i in 0..5 {
983 store
984 .store(MemoryItem::new(format!("item {i}")))
985 .await
986 .unwrap();
987 }
988 store.clear().await.unwrap();
989 assert_eq!(store.count().await.unwrap(), 0);
990 }
991
992 #[tokio::test]
993 async fn test_persistence_across_instances() {
994 let dir = TempDir::new().unwrap();
995 {
996 let store = FileMemoryStore::new(dir.path()).await.unwrap();
997 store
998 .store(MemoryItem::new("persistent data").with_tags(vec!["test".into()]))
999 .await
1000 .unwrap();
1001 }
1002 {
1003 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1004 assert_eq!(store.count().await.unwrap(), 1);
1005 assert_eq!(store.search("persistent", 10).await.unwrap().len(), 1);
1006 }
1007 }
1008
1009 #[tokio::test]
1010 async fn test_rebuild_index() {
1011 let dir = TempDir::new().unwrap();
1012 {
1013 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1014 store.store(MemoryItem::new("alpha")).await.unwrap();
1015 store.store(MemoryItem::new("beta")).await.unwrap();
1016 }
1017 tokio::fs::remove_file(dir.path().join("index.json"))
1018 .await
1019 .unwrap();
1020 {
1021 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1022 assert_eq!(store.count().await.unwrap(), 0);
1023 store.rebuild_index().await.unwrap();
1024 assert_eq!(store.count().await.unwrap(), 2);
1025 }
1026 }
1027
1028 #[tokio::test]
1029 async fn test_path_traversal_prevention() {
1030 let (_dir, store) = setup().await;
1031 let mut item = MemoryItem::new("sneaky");
1032 item.id = "../../../etc/passwd".to_string();
1033 store.store(item).await.unwrap();
1034 let results = store.search("sneaky", 10).await.unwrap();
1035 assert_eq!(results.len(), 1);
1036 assert!(!results[0].id.contains('/'));
1037 assert!(!results[0].id.contains(".."));
1038 }
1039
1040 #[tokio::test]
1041 async fn test_importance_threshold() {
1042 let (_dir, store) = setup().await;
1043 store
1044 .store(MemoryItem::new("low").with_importance(0.2))
1045 .await
1046 .unwrap();
1047 store
1048 .store(MemoryItem::new("high").with_importance(0.8))
1049 .await
1050 .unwrap();
1051 let results = store.get_important(0.5, 10).await.unwrap();
1052 assert_eq!(results.len(), 1);
1053 assert_eq!(results[0].content, "high");
1054 }
1055}