1use crate::{MemoryRow, Storage};
4use codemem_core::{
5 CodememError, ConsolidationLogEntry, Edge, GraphNode, MemoryNode, NodeKind, Session,
6 StorageBackend, StorageStats,
7};
8use rusqlite::params;
9use std::collections::HashMap;
10
11impl StorageBackend for Storage {
12 fn insert_memory(&self, memory: &MemoryNode) -> Result<(), CodememError> {
13 Storage::insert_memory(self, memory)
14 }
15
16 fn get_memory(&self, id: &str) -> Result<Option<MemoryNode>, CodememError> {
17 Storage::get_memory(self, id)
18 }
19
20 fn get_memories_batch(&self, ids: &[&str]) -> Result<Vec<MemoryNode>, CodememError> {
21 if ids.is_empty() {
22 return Ok(Vec::new());
23 }
24 let conn = self.conn();
25
26 let placeholders: Vec<String> = (1..=ids.len()).map(|i| format!("?{i}")).collect();
27 let sql = format!(
28 "SELECT id, content, memory_type, importance, confidence, access_count, content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at FROM memories WHERE id IN ({})",
29 placeholders.join(",")
30 );
31
32 let mut stmt = conn
33 .prepare(&sql)
34 .map_err(|e| CodememError::Storage(e.to_string()))?;
35
36 let params: Vec<&dyn rusqlite::types::ToSql> = ids
37 .iter()
38 .map(|id| id as &dyn rusqlite::types::ToSql)
39 .collect();
40
41 let rows = stmt
42 .query_map(params.as_slice(), |row| {
43 Ok(MemoryRow {
44 id: row.get(0)?,
45 content: row.get(1)?,
46 memory_type: row.get(2)?,
47 importance: row.get(3)?,
48 confidence: row.get(4)?,
49 access_count: row.get(5)?,
50 content_hash: row.get(6)?,
51 tags: row.get(7)?,
52 metadata: row.get(8)?,
53 namespace: row.get(9)?,
54 created_at: row.get(10)?,
55 updated_at: row.get(11)?,
56 last_accessed_at: row.get(12)?,
57 })
58 })
59 .map_err(|e| CodememError::Storage(e.to_string()))?;
60
61 let mut memories = Vec::new();
62 for row in rows {
63 let row = row.map_err(|e| CodememError::Storage(e.to_string()))?;
64 memories.push(row.into_memory_node()?);
65 }
66 Ok(memories)
67 }
68
69 fn update_memory(
70 &self,
71 id: &str,
72 content: &str,
73 importance: Option<f64>,
74 ) -> Result<(), CodememError> {
75 Storage::update_memory(self, id, content, importance)
76 }
77
78 fn delete_memory(&self, id: &str) -> Result<bool, CodememError> {
79 Storage::delete_memory(self, id)
80 }
81
82 fn list_memory_ids(&self) -> Result<Vec<String>, CodememError> {
83 Storage::list_memory_ids(self)
84 }
85
86 fn list_memory_ids_for_namespace(&self, namespace: &str) -> Result<Vec<String>, CodememError> {
87 Storage::list_memory_ids_for_namespace(self, namespace)
88 }
89
90 fn list_namespaces(&self) -> Result<Vec<String>, CodememError> {
91 Storage::list_namespaces(self)
92 }
93
94 fn memory_count(&self) -> Result<usize, CodememError> {
95 Storage::memory_count(self)
96 }
97
98 fn store_embedding(&self, memory_id: &str, embedding: &[f32]) -> Result<(), CodememError> {
99 Storage::store_embedding(self, memory_id, embedding)
100 }
101
102 fn get_embedding(&self, memory_id: &str) -> Result<Option<Vec<f32>>, CodememError> {
103 Storage::get_embedding(self, memory_id)
104 }
105
106 fn delete_embedding(&self, memory_id: &str) -> Result<bool, CodememError> {
107 let conn = self.conn();
108 let deleted = conn
109 .execute(
110 "DELETE FROM memory_embeddings WHERE memory_id = ?1",
111 [memory_id],
112 )
113 .map_err(|e| CodememError::Storage(e.to_string()))?;
114 Ok(deleted > 0)
115 }
116
117 fn list_all_embeddings(&self) -> Result<Vec<(String, Vec<f32>)>, CodememError> {
118 let conn = self.conn();
119 let mut stmt = conn
120 .prepare("SELECT memory_id, embedding FROM memory_embeddings")
121 .map_err(|e| CodememError::Storage(e.to_string()))?;
122 let rows = stmt
123 .query_map([], |row| {
124 let id: String = row.get(0)?;
125 let blob: Vec<u8> = row.get(1)?;
126 Ok((id, blob))
127 })
128 .map_err(|e| CodememError::Storage(e.to_string()))?;
129 let mut result = Vec::new();
130 for row in rows {
131 let (id, blob) = row.map_err(|e| CodememError::Storage(e.to_string()))?;
132 let floats: Vec<f32> = blob
133 .chunks_exact(4)
134 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
135 .collect();
136 result.push((id, floats));
137 }
138 Ok(result)
139 }
140
141 fn insert_graph_node(&self, node: &GraphNode) -> Result<(), CodememError> {
142 Storage::insert_graph_node(self, node)
143 }
144
145 fn get_graph_node(&self, id: &str) -> Result<Option<GraphNode>, CodememError> {
146 Storage::get_graph_node(self, id)
147 }
148
149 fn delete_graph_node(&self, id: &str) -> Result<bool, CodememError> {
150 Storage::delete_graph_node(self, id)
151 }
152
153 fn all_graph_nodes(&self) -> Result<Vec<GraphNode>, CodememError> {
154 Storage::all_graph_nodes(self)
155 }
156
157 fn insert_graph_edge(&self, edge: &Edge) -> Result<(), CodememError> {
158 Storage::insert_graph_edge(self, edge)
159 }
160
161 fn get_edges_for_node(&self, node_id: &str) -> Result<Vec<Edge>, CodememError> {
162 Storage::get_edges_for_node(self, node_id)
163 }
164
165 fn all_graph_edges(&self) -> Result<Vec<Edge>, CodememError> {
166 Storage::all_graph_edges(self)
167 }
168
169 fn delete_graph_edges_for_node(&self, node_id: &str) -> Result<usize, CodememError> {
170 Storage::delete_graph_edges_for_node(self, node_id)
171 }
172
173 fn start_session(&self, id: &str, namespace: Option<&str>) -> Result<(), CodememError> {
174 Storage::start_session(self, id, namespace)
175 }
176
177 fn end_session(&self, id: &str, summary: Option<&str>) -> Result<(), CodememError> {
178 Storage::end_session(self, id, summary)
179 }
180
181 fn list_sessions(
182 &self,
183 namespace: Option<&str>,
184 limit: usize,
185 ) -> Result<Vec<Session>, CodememError> {
186 self.list_sessions_with_limit(namespace, limit)
187 }
188
189 fn insert_consolidation_log(
190 &self,
191 cycle_type: &str,
192 affected_count: usize,
193 ) -> Result<(), CodememError> {
194 Storage::insert_consolidation_log(self, cycle_type, affected_count)
195 }
196
197 fn last_consolidation_runs(&self) -> Result<Vec<ConsolidationLogEntry>, CodememError> {
198 Storage::last_consolidation_runs(self)
199 }
200
201 fn get_repeated_searches(
202 &self,
203 min_count: usize,
204 namespace: Option<&str>,
205 ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
206 Storage::get_repeated_searches(self, min_count, namespace)
207 }
208
209 fn get_file_hotspots(
210 &self,
211 min_count: usize,
212 namespace: Option<&str>,
213 ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
214 Storage::get_file_hotspots(self, min_count, namespace)
215 }
216
217 fn get_tool_usage_stats(
218 &self,
219 namespace: Option<&str>,
220 ) -> Result<Vec<(String, usize)>, CodememError> {
221 let map = Storage::get_tool_usage_stats(self, namespace)?;
222 let mut vec: Vec<(String, usize)> = map.into_iter().collect();
223 vec.sort_by(|a, b| b.1.cmp(&a.1));
224 Ok(vec)
225 }
226
227 fn get_decision_chains(
228 &self,
229 min_count: usize,
230 namespace: Option<&str>,
231 ) -> Result<Vec<(String, usize, Vec<String>)>, CodememError> {
232 Storage::get_decision_chains(self, min_count, namespace)
233 }
234
235 fn decay_stale_memories(
236 &self,
237 threshold_ts: i64,
238 decay_factor: f64,
239 ) -> Result<usize, CodememError> {
240 let conn = self.conn();
241 let rows = conn
242 .execute(
243 "UPDATE memories SET importance = importance * ?1 WHERE last_accessed_at < ?2",
244 params![decay_factor, threshold_ts],
245 )
246 .map_err(|e| CodememError::Storage(e.to_string()))?;
247 Ok(rows)
248 }
249
250 fn list_memories_for_creative(
251 &self,
252 ) -> Result<Vec<(String, String, Vec<String>)>, CodememError> {
253 let conn = self.conn();
254 let mut stmt = conn
255 .prepare("SELECT id, memory_type, tags FROM memories ORDER BY created_at DESC")
256 .map_err(|e| CodememError::Storage(e.to_string()))?;
257
258 let rows = stmt
259 .query_map([], |row| {
260 Ok((
261 row.get::<_, String>(0)?,
262 row.get::<_, String>(1)?,
263 row.get::<_, String>(2)?,
264 ))
265 })
266 .map_err(|e| CodememError::Storage(e.to_string()))?
267 .collect::<Result<Vec<_>, _>>()
268 .map_err(|e| CodememError::Storage(e.to_string()))?;
269
270 Ok(rows
271 .into_iter()
272 .map(|(id, mtype, tags_json)| {
273 let tags: Vec<String> = serde_json::from_str(&tags_json).unwrap_or_default();
274 (id, mtype, tags)
275 })
276 .collect())
277 }
278
279 fn find_cluster_duplicates(&self) -> Result<Vec<(String, String, f64)>, CodememError> {
280 let conn = self.conn();
281 let mut stmt = conn
282 .prepare(
283 "SELECT a.id, b.id, 1.0 as similarity
284 FROM memories a
285 INNER JOIN memories b ON substr(a.content_hash, 1, 16) = substr(b.content_hash, 1, 16)
286 WHERE a.id < b.id",
287 )
288 .map_err(|e| CodememError::Storage(e.to_string()))?;
289
290 let rows = stmt
291 .query_map([], |row| {
292 Ok((
293 row.get::<_, String>(0)?,
294 row.get::<_, String>(1)?,
295 row.get::<_, f64>(2)?,
296 ))
297 })
298 .map_err(|e| CodememError::Storage(e.to_string()))?
299 .collect::<Result<Vec<_>, _>>()
300 .map_err(|e| CodememError::Storage(e.to_string()))?;
301
302 Ok(rows)
303 }
304
305 fn find_forgettable(&self, importance_threshold: f64) -> Result<Vec<String>, CodememError> {
306 let conn = self.conn();
307 let mut stmt = conn
308 .prepare(
309 "SELECT id FROM memories WHERE importance < ?1 AND access_count = 0 ORDER BY importance ASC, last_accessed_at ASC",
310 )
311 .map_err(|e| CodememError::Storage(e.to_string()))?;
312
313 let ids = stmt
314 .query_map(params![importance_threshold], |row| row.get(0))
315 .map_err(|e| CodememError::Storage(e.to_string()))?
316 .collect::<Result<Vec<String>, _>>()
317 .map_err(|e| CodememError::Storage(e.to_string()))?;
318
319 Ok(ids)
320 }
321
322 fn insert_memories_batch(&self, memories: &[MemoryNode]) -> Result<(), CodememError> {
323 let conn = self.conn();
324 let tx = conn
325 .unchecked_transaction()
326 .map_err(|e| CodememError::Storage(e.to_string()))?;
327
328 for memory in memories {
329 let tags_json = serde_json::to_string(&memory.tags)?;
330 let metadata_json = serde_json::to_string(&memory.metadata)?;
331
332 tx.execute(
333 "INSERT OR IGNORE INTO memories (id, content, memory_type, importance, confidence, access_count, content_hash, tags, metadata, namespace, created_at, updated_at, last_accessed_at)
334 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
335 params![
336 memory.id,
337 memory.content,
338 memory.memory_type.to_string(),
339 memory.importance,
340 memory.confidence,
341 memory.access_count,
342 memory.content_hash,
343 tags_json,
344 metadata_json,
345 memory.namespace,
346 memory.created_at.timestamp(),
347 memory.updated_at.timestamp(),
348 memory.last_accessed_at.timestamp(),
349 ],
350 )
351 .map_err(|e| CodememError::Storage(e.to_string()))?;
352 }
353
354 tx.commit()
355 .map_err(|e| CodememError::Storage(e.to_string()))?;
356 Ok(())
357 }
358
359 fn store_embeddings_batch(&self, items: &[(&str, &[f32])]) -> Result<(), CodememError> {
360 let conn = self.conn();
361 let tx = conn
362 .unchecked_transaction()
363 .map_err(|e| CodememError::Storage(e.to_string()))?;
364
365 for (id, embedding) in items {
366 let blob: Vec<u8> = embedding.iter().flat_map(|f| f.to_le_bytes()).collect();
367 tx.execute(
368 "INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding) VALUES (?1, ?2)",
369 params![id, blob],
370 )
371 .map_err(|e| CodememError::Storage(e.to_string()))?;
372 }
373
374 tx.commit()
375 .map_err(|e| CodememError::Storage(e.to_string()))?;
376 Ok(())
377 }
378
379 fn load_file_hashes(&self) -> Result<HashMap<String, String>, CodememError> {
380 let conn = self.conn();
381 let mut stmt = conn
382 .prepare("SELECT file_path, content_hash FROM file_hashes")
383 .map_err(|e| CodememError::Storage(e.to_string()))?;
384
385 let rows = stmt
386 .query_map([], |row| {
387 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
388 })
389 .map_err(|e| CodememError::Storage(e.to_string()))?
390 .collect::<Result<Vec<_>, _>>()
391 .map_err(|e| CodememError::Storage(e.to_string()))?;
392
393 Ok(rows.into_iter().collect())
394 }
395
396 fn save_file_hashes(&self, hashes: &HashMap<String, String>) -> Result<(), CodememError> {
397 let conn = self.conn();
398 let tx = conn
399 .unchecked_transaction()
400 .map_err(|e| CodememError::Storage(e.to_string()))?;
401
402 tx.execute("DELETE FROM file_hashes", [])
403 .map_err(|e| CodememError::Storage(e.to_string()))?;
404
405 for (path, hash) in hashes {
406 tx.execute(
407 "INSERT INTO file_hashes (file_path, content_hash) VALUES (?1, ?2)",
408 params![path, hash],
409 )
410 .map_err(|e| CodememError::Storage(e.to_string()))?;
411 }
412
413 tx.commit()
414 .map_err(|e| CodememError::Storage(e.to_string()))?;
415 Ok(())
416 }
417
418 fn insert_graph_nodes_batch(&self, nodes: &[GraphNode]) -> Result<(), CodememError> {
419 let conn = self.conn();
420 let tx = conn
421 .unchecked_transaction()
422 .map_err(|e| CodememError::Storage(e.to_string()))?;
423
424 for node in nodes {
425 let payload_json =
426 serde_json::to_string(&node.payload).unwrap_or_else(|_| "{}".to_string());
427 tx.execute(
428 "INSERT OR REPLACE INTO graph_nodes (id, kind, label, payload, centrality, memory_id, namespace)
429 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
430 params![
431 node.id,
432 node.kind.to_string(),
433 node.label,
434 payload_json,
435 node.centrality,
436 node.memory_id,
437 node.namespace,
438 ],
439 )
440 .map_err(|e| CodememError::Storage(e.to_string()))?;
441 }
442
443 tx.commit()
444 .map_err(|e| CodememError::Storage(e.to_string()))?;
445 Ok(())
446 }
447
448 fn insert_graph_edges_batch(&self, edges: &[Edge]) -> Result<(), CodememError> {
449 let conn = self.conn();
450 let tx = conn
451 .unchecked_transaction()
452 .map_err(|e| CodememError::Storage(e.to_string()))?;
453
454 for edge in edges {
455 let props_json =
456 serde_json::to_string(&edge.properties).unwrap_or_else(|_| "{}".to_string());
457 tx.execute(
458 "INSERT OR REPLACE INTO graph_edges (id, src, dst, relationship, weight, properties, created_at)
459 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
460 params![
461 edge.id,
462 edge.src,
463 edge.dst,
464 edge.relationship.to_string(),
465 edge.weight,
466 props_json,
467 edge.created_at.timestamp(),
468 ],
469 )
470 .map_err(|e| CodememError::Storage(e.to_string()))?;
471 }
472
473 tx.commit()
474 .map_err(|e| CodememError::Storage(e.to_string()))?;
475 Ok(())
476 }
477
478 fn find_unembedded_memories(&self) -> Result<Vec<(String, String)>, CodememError> {
479 let conn = self.conn();
480 let mut stmt = conn
481 .prepare(
482 "SELECT m.id, m.content FROM memories m
483 LEFT JOIN memory_embeddings me ON m.id = me.memory_id
484 WHERE me.memory_id IS NULL",
485 )
486 .map_err(|e| CodememError::Storage(e.to_string()))?;
487
488 let rows = stmt
489 .query_map([], |row| {
490 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
491 })
492 .map_err(|e| CodememError::Storage(e.to_string()))?
493 .collect::<Result<Vec<_>, _>>()
494 .map_err(|e| CodememError::Storage(e.to_string()))?;
495
496 Ok(rows)
497 }
498
499 fn search_graph_nodes(
500 &self,
501 query: &str,
502 namespace: Option<&str>,
503 limit: usize,
504 ) -> Result<Vec<GraphNode>, CodememError> {
505 let conn = self.conn();
506 let pattern = format!("%{}%", query.to_lowercase());
507
508 let (sql, params_vec): (String, Vec<Box<dyn rusqlite::types::ToSql>>) =
509 if let Some(ns) = namespace {
510 (
511 "SELECT id, kind, label, payload, centrality, memory_id, namespace \
512 FROM graph_nodes WHERE LOWER(label) LIKE ?1 AND namespace = ?2 \
513 ORDER BY centrality DESC LIMIT ?3"
514 .to_string(),
515 vec![
516 Box::new(pattern) as Box<dyn rusqlite::types::ToSql>,
517 Box::new(ns.to_string()),
518 Box::new(limit as i64),
519 ],
520 )
521 } else {
522 (
523 "SELECT id, kind, label, payload, centrality, memory_id, namespace \
524 FROM graph_nodes WHERE LOWER(label) LIKE ?1 \
525 ORDER BY centrality DESC LIMIT ?2"
526 .to_string(),
527 vec![
528 Box::new(pattern) as Box<dyn rusqlite::types::ToSql>,
529 Box::new(limit as i64),
530 ],
531 )
532 };
533
534 let refs: Vec<&dyn rusqlite::types::ToSql> =
535 params_vec.iter().map(|p| p.as_ref()).collect();
536 let mut stmt = conn
537 .prepare(&sql)
538 .map_err(|e| CodememError::Storage(e.to_string()))?;
539
540 let rows = stmt
541 .query_map(refs.as_slice(), |row| {
542 let kind_str: String = row.get(1)?;
543 let payload_str: String = row.get(3)?;
544 Ok(GraphNode {
545 id: row.get(0)?,
546 kind: kind_str.parse().unwrap_or(NodeKind::Memory),
547 label: row.get(2)?,
548 payload: serde_json::from_str(&payload_str).unwrap_or_default(),
549 centrality: row.get(4)?,
550 memory_id: row.get(5)?,
551 namespace: row.get(6)?,
552 })
553 })
554 .map_err(|e| CodememError::Storage(e.to_string()))?
555 .collect::<Result<Vec<_>, _>>()
556 .map_err(|e| CodememError::Storage(e.to_string()))?;
557
558 Ok(rows)
559 }
560
561 fn list_memories_filtered(
562 &self,
563 namespace: Option<&str>,
564 memory_type: Option<&str>,
565 ) -> Result<Vec<MemoryNode>, CodememError> {
566 let conn = self.conn();
567 let mut sql = "SELECT id, content, memory_type, importance, confidence, access_count, \
568 content_hash, tags, metadata, namespace, created_at, updated_at, \
569 last_accessed_at FROM memories WHERE 1=1"
570 .to_string();
571 let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
572
573 if let Some(ns) = namespace {
574 param_values.push(Box::new(ns.to_string()));
575 sql.push_str(&format!(" AND namespace = ?{}", param_values.len()));
576 }
577 if let Some(mt) = memory_type {
578 param_values.push(Box::new(mt.to_string()));
579 sql.push_str(&format!(" AND memory_type = ?{}", param_values.len()));
580 }
581 sql.push_str(" ORDER BY created_at DESC");
582
583 let refs: Vec<&dyn rusqlite::types::ToSql> =
584 param_values.iter().map(|p| p.as_ref()).collect();
585 let mut stmt = conn
586 .prepare(&sql)
587 .map_err(|e| CodememError::Storage(e.to_string()))?;
588
589 let rows = stmt
590 .query_map(refs.as_slice(), |row| {
591 Ok(MemoryRow {
592 id: row.get(0)?,
593 content: row.get(1)?,
594 memory_type: row.get(2)?,
595 importance: row.get(3)?,
596 confidence: row.get(4)?,
597 access_count: row.get(5)?,
598 content_hash: row.get(6)?,
599 tags: row.get(7)?,
600 metadata: row.get(8)?,
601 namespace: row.get(9)?,
602 created_at: row.get(10)?,
603 updated_at: row.get(11)?,
604 last_accessed_at: row.get(12)?,
605 })
606 })
607 .map_err(|e| CodememError::Storage(e.to_string()))?;
608
609 let mut result = Vec::new();
610 for row in rows {
611 let mr = row.map_err(|e| CodememError::Storage(e.to_string()))?;
612 result.push(mr.into_memory_node()?);
613 }
614
615 Ok(result)
616 }
617
618 fn graph_edges_for_namespace(&self, namespace: &str) -> Result<Vec<Edge>, CodememError> {
619 Storage::graph_edges_for_namespace(self, namespace)
620 }
621
622 fn stats(&self) -> Result<StorageStats, CodememError> {
623 Storage::stats(self)
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use crate::Storage;
630 use codemem_core::{MemoryNode, MemoryType, StorageBackend};
631 use std::collections::HashMap;
632
633 fn test_memory() -> MemoryNode {
634 let now = chrono::Utc::now();
635 let content = "Test memory content";
636 MemoryNode {
637 id: uuid::Uuid::new_v4().to_string(),
638 content: content.to_string(),
639 memory_type: MemoryType::Context,
640 importance: 0.7,
641 confidence: 1.0,
642 access_count: 0,
643 content_hash: Storage::content_hash(content),
644 tags: vec!["test".to_string()],
645 metadata: HashMap::new(),
646 namespace: None,
647 created_at: now,
648 updated_at: now,
649 last_accessed_at: now,
650 }
651 }
652
653 #[test]
654 fn get_memories_batch_returns_multiple() {
655 let storage = Storage::open_in_memory().unwrap();
656 let m1 = test_memory();
657 let mut m2 = test_memory();
658 m2.id = uuid::Uuid::new_v4().to_string();
659 m2.content = "Different content".to_string();
660 m2.content_hash = Storage::content_hash(&m2.content);
661
662 storage.insert_memory(&m1).unwrap();
663 storage.insert_memory(&m2).unwrap();
664
665 let backend: &dyn StorageBackend = &storage;
666 let batch = backend.get_memories_batch(&[&m1.id, &m2.id]).unwrap();
667 assert_eq!(batch.len(), 2);
668 }
669
670 #[test]
671 fn get_memories_batch_empty() {
672 let storage = Storage::open_in_memory().unwrap();
673 let backend: &dyn StorageBackend = &storage;
674 let batch = backend.get_memories_batch(&[]).unwrap();
675 assert!(batch.is_empty());
676 }
677
678 #[test]
679 fn storage_backend_trait_object() {
680 let storage = Storage::open_in_memory().unwrap();
681 let backend: Box<dyn StorageBackend> = Box::new(storage);
682
683 let m = test_memory();
684 backend.insert_memory(&m).unwrap();
685 let retrieved = backend.get_memory(&m.id).unwrap().unwrap();
686 assert_eq!(retrieved.id, m.id);
687 }
688
689 #[test]
690 fn file_hashes_roundtrip() {
691 let storage = Storage::open_in_memory().unwrap();
692 let backend: &dyn StorageBackend = &storage;
693
694 let mut hashes = HashMap::new();
695 hashes.insert("src/main.rs".to_string(), "abc123".to_string());
696 hashes.insert("src/lib.rs".to_string(), "def456".to_string());
697
698 backend.save_file_hashes(&hashes).unwrap();
699 let loaded = backend.load_file_hashes().unwrap();
700 assert_eq!(loaded.len(), 2);
701 assert_eq!(loaded.get("src/main.rs"), Some(&"abc123".to_string()));
702 }
703
704 #[test]
705 fn decay_stale_memories_updates() {
706 let storage = Storage::open_in_memory().unwrap();
707 let backend: &dyn StorageBackend = &storage;
708
709 let m = test_memory();
710 backend.insert_memory(&m).unwrap();
711
712 let count = backend.decay_stale_memories(0, 0.5).unwrap();
714 assert_eq!(count, 0);
715
716 let count = backend.decay_stale_memories(i64::MAX, 0.5).unwrap();
718 assert_eq!(count, 1);
719 }
720
721 #[test]
722 fn find_forgettable_returns_low_importance() {
723 let storage = Storage::open_in_memory().unwrap();
724 let backend: &dyn StorageBackend = &storage;
725
726 let mut m = test_memory();
727 m.importance = 0.1;
728 backend.insert_memory(&m).unwrap();
729
730 let forgettable = backend.find_forgettable(0.5).unwrap();
731 assert_eq!(forgettable.len(), 1);
732 assert_eq!(forgettable[0], m.id);
733
734 let forgettable = backend.find_forgettable(0.05).unwrap();
735 assert!(forgettable.is_empty());
736 }
737}