1use std::sync::Mutex;
11
12use async_trait::async_trait;
13
14use super::{EmbeddingProvider, MemoryEntry, VectorStore};
15use crate::error::Result;
16
17pub struct NoopEmbedding;
26
27#[async_trait]
28impl EmbeddingProvider for NoopEmbedding {
29 async fn embed(&self, _text: &str) -> Vec<f32> {
30 vec![]
31 }
32}
33
34pub struct NoopVectorStore {
45 entries: Mutex<Vec<MemoryEntry>>,
46}
47
48impl NoopVectorStore {
49 pub fn new() -> Self {
50 Self {
51 entries: Mutex::new(Vec::new()),
52 }
53 }
54}
55
56impl Default for NoopVectorStore {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62#[async_trait]
63impl VectorStore for NoopVectorStore {
64 async fn upsert(&self, entry: &MemoryEntry, _vector: Vec<f32>) -> Result<()> {
65 let mut entries = self.entries.lock().unwrap();
66 if let Some(existing) = entries.iter_mut().find(|e| e.id == entry.id) {
67 *existing = entry.clone();
68 } else {
69 entries.push(entry.clone());
70 }
71 Ok(())
72 }
73
74 async fn search(
75 &self,
76 _query_vec: Vec<f32>,
77 query_text: &str,
78 limit: usize,
79 ) -> Result<Vec<MemoryEntry>> {
80 let entries = self.entries.lock().unwrap();
81 let q = query_text.to_lowercase();
82 let matches: Vec<MemoryEntry> = entries
83 .iter()
84 .filter(|e| {
85 e.text.to_lowercase().contains(&q)
86 || e.tags.iter().any(|t| t.to_lowercase().contains(&q))
87 })
88 .take(limit)
89 .cloned()
90 .collect();
91 Ok(matches)
92 }
93
94 async fn remove(&self, id: &str) -> Result<()> {
95 let mut entries = self.entries.lock().unwrap();
96 entries.retain(|e| e.id != id);
97 Ok(())
98 }
99
100 async fn list_all(&self) -> Result<Vec<MemoryEntry>> {
101 let entries = self.entries.lock().unwrap();
102 Ok(entries.clone())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[tokio::test]
111 async fn noop_embedding_returns_empty() {
112 let emb = NoopEmbedding;
113 assert!(emb.embed("hello world").await.is_empty());
114 }
115
116 #[tokio::test]
117 async fn noop_store_upsert_and_search() {
118 let store = NoopVectorStore::new();
119
120 let entry = MemoryEntry {
121 id: "N1".into(),
122 text: "Rust is a systems language".into(),
123 tags: vec!["rust".into()],
124 ts: "2026-01-01T00:00:00Z".into(),
125 };
126 store.upsert(&entry, vec![]).await.unwrap();
127
128 let results = store.search(vec![], "systems", 5).await.unwrap();
129 assert_eq!(results.len(), 1);
130 assert_eq!(results[0].id, "N1");
131
132 let no_results = store.search(vec![], "python", 5).await.unwrap();
133 assert!(no_results.is_empty());
134 }
135
136 #[tokio::test]
137 async fn noop_store_overwrite_on_same_id() {
138 let store = NoopVectorStore::new();
139
140 let e1 = MemoryEntry {
141 id: "N1".into(),
142 text: "original".into(),
143 tags: vec![],
144 ts: "2026-01-01T00:00:00Z".into(),
145 };
146 let e2 = MemoryEntry {
147 id: "N1".into(),
148 text: "updated".into(),
149 tags: vec![],
150 ts: "2026-01-02T00:00:00Z".into(),
151 };
152 store.upsert(&e1, vec![]).await.unwrap();
153 store.upsert(&e2, vec![]).await.unwrap();
154
155 let all = store.list_all().await.unwrap();
156 assert_eq!(all.len(), 1);
157 assert_eq!(all[0].text, "updated");
158 }
159
160 #[tokio::test]
161 async fn noop_store_remove_by_id() {
162 let store = NoopVectorStore::new();
163 let e = MemoryEntry {
164 id: "N1".into(),
165 text: "to be deleted".into(),
166 tags: vec![],
167 ts: "2026-01-01T00:00:00Z".into(),
168 };
169 store.upsert(&e, vec![]).await.unwrap();
170 store.remove("N1").await.unwrap();
171 assert!(store.list_all().await.unwrap().is_empty());
172 }
173}