Skip to main content

a3s_memory/
lib.rs

1//! A3S Memory — pluggable memory storage for AI agents.
2//!
3//! Provides the `MemoryStore` trait, `MemoryItem`, `MemoryType`,
4//! configuration types, and a `FileMemoryStore` default implementation.
5
6use anyhow::Context as _;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use tokio::sync::RwLock;
11
12// ============================================================================
13// Configuration
14// ============================================================================
15
16/// Configuration for relevance scoring
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct RelevanceConfig {
20    /// Exponential decay half-life in days (default: 30.0)
21    #[serde(default = "RelevanceConfig::default_decay_days")]
22    pub decay_days: f32,
23    /// Weight for importance factor (default: 0.7)
24    #[serde(default = "RelevanceConfig::default_importance_weight")]
25    pub importance_weight: f32,
26    /// Weight for recency factor (default: 0.3)
27    #[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// ============================================================================
54// Memory Item
55// ============================================================================
56
57/// A single memory item
58#[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    /// Calculate relevance score at a given timestamp using the provided config
122    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    /// Calculate relevance score with default config
129    pub fn relevance_score(&self) -> f32 {
130        self.relevance_score_at(Utc::now(), &RelevanceConfig::default())
131    }
132}
133
134/// Type of memory
135#[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// ============================================================================
145// Memory Store Trait
146// ============================================================================
147
148#[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
165// ============================================================================
166// Shared helpers
167// ============================================================================
168
169/// Score an index entry for sorting (avoids loading full MemoryItem from disk)
170fn 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
186// ============================================================================
187// In-Memory Store
188// ============================================================================
189
190/// In-memory `MemoryStore` implementation.
191///
192/// Useful for testing and ephemeral (non-persistent) use cases.
193pub 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// ============================================================================
308// File-Based Memory Store
309// ============================================================================
310
311#[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
334/// File-based memory store with atomic writes and in-memory index.
335///
336/// ```text
337/// memory_dir/
338///   index.json
339///   items/{id}.json
340/// ```
341pub 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    /// Rebuild the index from item files on disk (useful for corruption recovery).
414    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// ============================================================================
589// Tests
590// ============================================================================
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    // MemoryItem tests
597
598    #[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    // relevance_score_at tests
659
660    #[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        // High importance weight → score dominated by importance
666        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        // Short decay → recent item still scores well
675        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(); // 30-day half-life
689        let score = old_item.relevance_score_at(Utc::now(), &config);
690        // After 60 days (2 half-lives), decay ≈ exp(-2) ≈ 0.135
691        // score ≈ 0.5*0.7 + 0.135*0.3 ≈ 0.39
692        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    // RelevanceConfig tests
703
704    #[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    // InMemoryStore tests
713
714    #[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}