Skip to main content

brainwires_knowledge/knowledge/bks_pks/personal/
cache.rs

1//! Local cache for personal facts with SQLite persistence
2//!
3//! Maintains a local copy of personal facts synced from the server, with offline
4//! queue support for when the server is unavailable.
5
6use super::fact::{
7    PendingFactSubmission, PersonalFact, PersonalFactCategory, PersonalFactFeedback,
8};
9use anyhow::Result;
10use rusqlite::{Connection, OptionalExtension, params};
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::{Arc, Mutex};
14
15/// Local cache of personal facts synced from server
16pub struct PersonalKnowledgeCache {
17    /// SQLite connection
18    conn: Arc<Mutex<Connection>>,
19
20    /// In-memory cache for fast access
21    facts: HashMap<String, PersonalFact>,
22
23    /// Index by key for fast lookup
24    facts_by_key: HashMap<String, String>, // key -> id
25
26    /// Timestamp of last successful sync with server
27    pub last_sync: i64,
28
29    /// Queue of facts waiting to be submitted to server
30    pending_submissions: Vec<PendingFactSubmission>,
31
32    /// Queue of feedback waiting to be sent to server
33    pending_feedback: Vec<PersonalFactFeedback>,
34
35    /// Maximum size of offline queue
36    max_queue_size: usize,
37}
38
39impl PersonalKnowledgeCache {
40    /// Create a new cache with SQLite persistence
41    pub fn new<P: AsRef<Path>>(db_path: P, max_queue_size: usize) -> Result<Self> {
42        let conn = Connection::open(db_path)?;
43        Self::init_schema(&conn)?;
44
45        let mut cache = Self {
46            conn: Arc::new(Mutex::new(conn)),
47            facts: HashMap::new(),
48            facts_by_key: HashMap::new(),
49            last_sync: 0,
50            pending_submissions: Vec::new(),
51            pending_feedback: Vec::new(),
52            max_queue_size,
53        };
54
55        // Load existing data from database
56        cache.load_from_db()?;
57
58        Ok(cache)
59    }
60
61    /// Create an in-memory cache (for testing)
62    pub fn in_memory(max_queue_size: usize) -> Result<Self> {
63        let conn = Connection::open_in_memory()?;
64        Self::init_schema(&conn)?;
65
66        Ok(Self {
67            conn: Arc::new(Mutex::new(conn)),
68            facts: HashMap::new(),
69            facts_by_key: HashMap::new(),
70            last_sync: 0,
71            pending_submissions: Vec::new(),
72            pending_feedback: Vec::new(),
73            max_queue_size,
74        })
75    }
76
77    /// Initialize database schema
78    fn init_schema(conn: &Connection) -> Result<()> {
79        conn.execute_batch(
80            r#"
81            CREATE TABLE IF NOT EXISTS personal_facts (
82                id TEXT PRIMARY KEY,
83                category TEXT NOT NULL,
84                key TEXT NOT NULL UNIQUE,
85                value TEXT NOT NULL,
86                context TEXT,
87                confidence REAL NOT NULL,
88                reinforcements INTEGER NOT NULL DEFAULT 0,
89                contradictions INTEGER NOT NULL DEFAULT 0,
90                last_used INTEGER NOT NULL,
91                created_at INTEGER NOT NULL,
92                updated_at INTEGER NOT NULL,
93                source TEXT NOT NULL,
94                version INTEGER NOT NULL DEFAULT 1,
95                deleted INTEGER NOT NULL DEFAULT 0,
96                local_only INTEGER NOT NULL DEFAULT 0
97            );
98
99            CREATE INDEX IF NOT EXISTS idx_personal_facts_key ON personal_facts(key);
100            CREATE INDEX IF NOT EXISTS idx_personal_facts_category ON personal_facts(category);
101            CREATE INDEX IF NOT EXISTS idx_personal_facts_confidence ON personal_facts(confidence);
102
103            CREATE TABLE IF NOT EXISTS pending_fact_submissions (
104                id INTEGER PRIMARY KEY AUTOINCREMENT,
105                fact_json TEXT NOT NULL,
106                queued_at INTEGER NOT NULL,
107                attempts INTEGER NOT NULL DEFAULT 0,
108                last_error TEXT
109            );
110
111            CREATE TABLE IF NOT EXISTS pending_fact_feedback (
112                id INTEGER PRIMARY KEY AUTOINCREMENT,
113                fact_id TEXT NOT NULL,
114                is_reinforcement INTEGER NOT NULL,
115                context TEXT,
116                timestamp INTEGER NOT NULL
117            );
118
119            CREATE TABLE IF NOT EXISTS personal_sync_state (
120                key TEXT PRIMARY KEY,
121                value TEXT NOT NULL
122            );
123            "#,
124        )?;
125
126        Ok(())
127    }
128
129    /// Load facts and state from database
130    fn load_from_db(&mut self) -> Result<()> {
131        let conn = self
132            .conn
133            .lock()
134            .expect("personal knowledge cache connection lock poisoned");
135
136        // Load last sync timestamp
137        self.last_sync = conn
138            .query_row(
139                "SELECT value FROM personal_sync_state WHERE key = 'last_sync'",
140                [],
141                |row| row.get::<_, String>(0),
142            )
143            .optional()?
144            .and_then(|s| s.parse().ok())
145            .unwrap_or(0);
146
147        // Load facts
148        let mut stmt = conn.prepare(
149            "SELECT id, category, key, value, context, confidence,
150                    reinforcements, contradictions, last_used, created_at,
151                    updated_at, source, version, deleted, local_only
152             FROM personal_facts WHERE deleted = 0",
153        )?;
154
155        let facts = stmt.query_map([], |row| {
156            Ok(PersonalFact {
157                id: row.get(0)?,
158                category: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(1)?))
159                    .unwrap_or(PersonalFactCategory::Preference),
160                key: row.get(2)?,
161                value: row.get(3)?,
162                context: row.get(4)?,
163                confidence: row.get(5)?,
164                reinforcements: row.get(6)?,
165                contradictions: row.get(7)?,
166                last_used: row.get(8)?,
167                created_at: row.get(9)?,
168                updated_at: row.get(10)?,
169                source: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(11)?))
170                    .unwrap_or(super::fact::PersonalFactSource::ExplicitStatement),
171                version: row.get::<_, i64>(12)? as u64,
172                deleted: row.get::<_, i32>(13)? != 0,
173                local_only: row.get::<_, i32>(14)? != 0,
174            })
175        })?;
176
177        for fact in facts {
178            let fact = fact?;
179            self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
180            self.facts.insert(fact.id.clone(), fact);
181        }
182
183        // Load pending submissions
184        let mut stmt = conn.prepare(
185            "SELECT fact_json, queued_at, attempts, last_error FROM pending_fact_submissions",
186        )?;
187
188        let submissions = stmt.query_map([], |row| {
189            let json: String = row.get(0)?;
190            let fact: PersonalFact = serde_json::from_str(&json).map_err(|e| {
191                rusqlite::Error::FromSqlConversionFailure(
192                    0,
193                    rusqlite::types::Type::Text,
194                    Box::new(e),
195                )
196            })?;
197            Ok(PendingFactSubmission {
198                fact,
199                queued_at: row.get(1)?,
200                attempts: row.get(2)?,
201                last_error: row.get(3)?,
202            })
203        })?;
204
205        for submission in submissions {
206            self.pending_submissions.push(submission?);
207        }
208
209        // Load pending feedback
210        let mut stmt = conn.prepare(
211            "SELECT fact_id, is_reinforcement, context, timestamp FROM pending_fact_feedback",
212        )?;
213
214        let feedback = stmt.query_map([], |row| {
215            Ok(PersonalFactFeedback {
216                fact_id: row.get(0)?,
217                is_reinforcement: row.get::<_, i32>(1)? != 0,
218                context: row.get(2)?,
219                timestamp: row.get(3)?,
220            })
221        })?;
222
223        for fb in feedback {
224            self.pending_feedback.push(fb?);
225        }
226
227        Ok(())
228    }
229
230    /// Save a fact to the database
231    fn save_fact_to_db(&self, fact: &PersonalFact) -> Result<()> {
232        let conn = self
233            .conn
234            .lock()
235            .expect("personal knowledge cache connection lock poisoned");
236        let category = serde_json::to_string(&fact.category)?
237            .trim_matches('"')
238            .to_string();
239        let source = serde_json::to_string(&fact.source)?
240            .trim_matches('"')
241            .to_string();
242
243        conn.execute(
244            r#"INSERT OR REPLACE INTO personal_facts
245               (id, category, key, value, context, confidence,
246                reinforcements, contradictions, last_used, created_at,
247                updated_at, source, version, deleted, local_only)
248               VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)"#,
249            params![
250                fact.id,
251                category,
252                fact.key,
253                fact.value,
254                fact.context,
255                fact.confidence,
256                fact.reinforcements,
257                fact.contradictions,
258                fact.last_used,
259                fact.created_at,
260                fact.updated_at,
261                source,
262                fact.version as i64,
263                fact.deleted as i32,
264                fact.local_only as i32,
265            ],
266        )?;
267
268        Ok(())
269    }
270
271    /// Update last sync timestamp
272    pub fn set_last_sync(&mut self, timestamp: i64) -> Result<()> {
273        self.last_sync = timestamp;
274        let conn = self
275            .conn
276            .lock()
277            .expect("personal knowledge cache connection lock poisoned");
278        conn.execute(
279            "INSERT OR REPLACE INTO personal_sync_state (key, value) VALUES ('last_sync', ?1)",
280            params![timestamp.to_string()],
281        )?;
282        Ok(())
283    }
284
285    /// Add or update a fact (upsert by key)
286    pub fn upsert_fact(&mut self, mut fact: PersonalFact) -> Result<()> {
287        // Check if we already have a fact with this key
288        if let Some(existing_id) = self.facts_by_key.get(&fact.key)
289            && let Some(existing) = self.facts.get(existing_id)
290        {
291            // Update existing fact
292            fact.id = existing.id.clone();
293            fact.reinforcements = existing.reinforcements + 1;
294            fact.confidence = fact.confidence.max(existing.confidence);
295        }
296
297        self.save_fact_to_db(&fact)?;
298        self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
299        self.facts.insert(fact.id.clone(), fact);
300        Ok(())
301    }
302
303    /// Add a new fact to the cache
304    pub fn add_fact(&mut self, fact: PersonalFact) -> Result<()> {
305        self.save_fact_to_db(&fact)?;
306        self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
307        self.facts.insert(fact.id.clone(), fact);
308        Ok(())
309    }
310
311    /// Add or update a fact with simplified interface
312    pub fn upsert_fact_simple(
313        &mut self,
314        key: &str,
315        value: &str,
316        _confidence: f32,
317        local_only: bool,
318    ) -> Result<()> {
319        use super::fact::{PersonalFactCategory, PersonalFactSource};
320
321        let fact = PersonalFact::new(
322            PersonalFactCategory::Context,
323            key.to_string(),
324            value.to_string(),
325            None,
326            PersonalFactSource::SystemObserved,
327            local_only,
328        );
329
330        self.upsert_fact(fact)
331    }
332
333    /// Get all non-deleted facts
334    pub fn get_all_facts(&self) -> Vec<&PersonalFact> {
335        self.facts.values().filter(|f| !f.deleted).collect()
336    }
337
338    /// Get facts by key prefix (e.g., "recent_entity:" gets all recent entity facts)
339    pub fn get_facts_by_key_prefix(&self, prefix: &str) -> Result<Vec<&PersonalFact>> {
340        Ok(self
341            .facts
342            .values()
343            .filter(|f| !f.deleted && f.key.starts_with(prefix))
344            .collect())
345    }
346
347    /// Update an existing fact
348    pub fn update_fact(&mut self, fact: PersonalFact) -> Result<()> {
349        self.save_fact_to_db(&fact)?;
350        self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
351        self.facts.insert(fact.id.clone(), fact);
352        Ok(())
353    }
354
355    /// Get a fact by ID
356    pub fn get_fact(&self, id: &str) -> Option<&PersonalFact> {
357        self.facts.get(id)
358    }
359
360    /// Get a fact by key
361    pub fn get_fact_by_key(&self, key: &str) -> Option<&PersonalFact> {
362        self.facts_by_key.get(key).and_then(|id| self.facts.get(id))
363    }
364
365    /// Get a mutable reference to a fact by ID
366    pub fn get_fact_mut(&mut self, id: &str) -> Option<&mut PersonalFact> {
367        self.facts.get_mut(id)
368    }
369
370    /// Remove a fact (soft delete)
371    pub fn remove_fact(&mut self, id: &str) -> Result<bool> {
372        // First check if fact exists and get key
373        let key_to_remove = {
374            if let Some(fact) = self.facts.get_mut(id) {
375                fact.delete();
376                Some(fact.key.clone())
377            } else {
378                None
379            }
380        };
381
382        // Now save and remove from index
383        if let Some(key) = key_to_remove {
384            if let Some(fact) = self.facts.get(id) {
385                self.save_fact_to_db(fact)?;
386            }
387            self.facts_by_key.remove(&key);
388            return Ok(true);
389        }
390        Ok(false)
391    }
392
393    /// Remove a fact by key (soft delete)
394    pub fn remove_fact_by_key(&mut self, key: &str) -> Result<bool> {
395        if let Some(id) = self.facts_by_key.get(key).cloned() {
396            return self.remove_fact(&id);
397        }
398        Ok(false)
399    }
400
401    /// Get all active facts
402    pub fn all_facts(&self) -> impl Iterator<Item = &PersonalFact> {
403        self.facts.values().filter(|f| !f.deleted)
404    }
405
406    /// Get facts by category
407    pub fn facts_by_category(&self, category: PersonalFactCategory) -> Vec<&PersonalFact> {
408        self.facts
409            .values()
410            .filter(|f| !f.deleted && f.category == category)
411            .collect()
412    }
413
414    /// Get facts matching a search query (simple substring match)
415    pub fn search_facts(&self, query: &str) -> Vec<&PersonalFact> {
416        let query_lower = query.to_lowercase();
417        self.facts
418            .values()
419            .filter(|f| {
420                !f.deleted
421                    && (f.key.to_lowercase().contains(&query_lower)
422                        || f.value.to_lowercase().contains(&query_lower))
423            })
424            .collect()
425    }
426
427    /// Get facts above a confidence threshold
428    pub fn get_reliable_facts(&self, min_confidence: f32) -> Vec<&PersonalFact> {
429        self.facts
430            .values()
431            .filter(|f| !f.deleted && f.is_reliable(min_confidence))
432            .collect()
433    }
434
435    /// Get facts that should be synced to server (not local_only)
436    pub fn get_syncable_facts(&self) -> Vec<&PersonalFact> {
437        self.facts
438            .values()
439            .filter(|f| !f.deleted && !f.local_only)
440            .collect()
441    }
442
443    /// Queue a fact for submission to server
444    pub fn queue_submission(&mut self, fact: PersonalFact) -> Result<bool> {
445        if fact.local_only {
446            return Ok(false); // Never sync local-only facts
447        }
448
449        if self.pending_submissions.len() >= self.max_queue_size {
450            return Ok(false);
451        }
452
453        let submission = PendingFactSubmission::new(fact);
454        let json = serde_json::to_string(&submission.fact)?;
455
456        let conn = self
457            .conn
458            .lock()
459            .expect("personal knowledge cache connection lock poisoned");
460        conn.execute(
461            "INSERT INTO pending_fact_submissions (fact_json, queued_at, attempts) VALUES (?1, ?2, ?3)",
462            params![json, submission.queued_at, submission.attempts],
463        )?;
464
465        self.pending_submissions.push(submission);
466        Ok(true)
467    }
468
469    /// Get pending submissions
470    pub fn pending_submissions(&self) -> &[PendingFactSubmission] {
471        &self.pending_submissions
472    }
473
474    /// Clear all pending submissions (after successful sync)
475    pub fn clear_pending_submissions(&mut self) -> Result<()> {
476        self.pending_submissions.clear();
477        let conn = self
478            .conn
479            .lock()
480            .expect("personal knowledge cache connection lock poisoned");
481        conn.execute("DELETE FROM pending_fact_submissions", [])?;
482        Ok(())
483    }
484
485    /// Queue feedback for sending to server
486    pub fn queue_feedback(&mut self, feedback: PersonalFactFeedback) -> Result<bool> {
487        // Check if the fact is local-only
488        if let Some(fact) = self.facts.get(&feedback.fact_id)
489            && fact.local_only
490        {
491            return Ok(false); // Don't sync feedback for local-only facts
492        }
493
494        if self.pending_feedback.len() >= self.max_queue_size {
495            return Ok(false);
496        }
497
498        let conn = self
499            .conn
500            .lock()
501            .expect("personal knowledge cache connection lock poisoned");
502        conn.execute(
503            "INSERT INTO pending_fact_feedback (fact_id, is_reinforcement, context, timestamp)
504             VALUES (?1, ?2, ?3, ?4)",
505            params![
506                feedback.fact_id,
507                feedback.is_reinforcement as i32,
508                feedback.context,
509                feedback.timestamp,
510            ],
511        )?;
512
513        self.pending_feedback.push(feedback);
514        Ok(true)
515    }
516
517    /// Get pending feedback
518    pub fn pending_feedback(&self) -> &[PersonalFactFeedback] {
519        &self.pending_feedback
520    }
521
522    /// Clear all pending feedback (after successful sync)
523    pub fn clear_pending_feedback(&mut self) -> Result<()> {
524        self.pending_feedback.clear();
525        let conn = self
526            .conn
527            .lock()
528            .expect("personal knowledge cache connection lock poisoned");
529        conn.execute("DELETE FROM pending_fact_feedback", [])?;
530        Ok(())
531    }
532
533    /// Merge facts from server (handles version conflicts)
534    pub fn merge_from_server(&mut self, server_facts: Vec<PersonalFact>) -> Result<MergeResult> {
535        let mut added = 0;
536        let mut updated = 0;
537        let mut conflicts = 0;
538
539        for server_fact in server_facts {
540            // Skip local-only facts that somehow got synced
541            if server_fact.local_only {
542                continue;
543            }
544
545            if let Some(local_fact) = self.facts.get(&server_fact.id) {
546                // Check for version conflict
547                if server_fact.version > local_fact.version {
548                    // Server wins - update local
549                    self.save_fact_to_db(&server_fact)?;
550                    self.facts_by_key
551                        .insert(server_fact.key.clone(), server_fact.id.clone());
552                    self.facts.insert(server_fact.id.clone(), server_fact);
553                    updated += 1;
554                } else if server_fact.version < local_fact.version {
555                    // Local is newer - conflict (should be rare)
556                    conflicts += 1;
557                }
558                // Equal versions - no action needed
559            } else {
560                // New fact from server
561                self.save_fact_to_db(&server_fact)?;
562                self.facts_by_key
563                    .insert(server_fact.key.clone(), server_fact.id.clone());
564                self.facts.insert(server_fact.id.clone(), server_fact);
565                added += 1;
566            }
567        }
568
569        Ok(MergeResult {
570            added,
571            updated,
572            conflicts,
573        })
574    }
575
576    /// Apply decay to all facts based on category
577    pub fn apply_decay(&mut self) -> Result<u32> {
578        let mut decayed = 0;
579
580        for fact in self.facts.values_mut() {
581            let old_confidence = fact.confidence;
582            fact.apply_decay();
583            if (fact.confidence - old_confidence).abs() > 0.001 {
584                decayed += 1;
585            }
586        }
587
588        // Save decayed facts to database
589        if decayed > 0 {
590            for fact in self.facts.values() {
591                self.save_fact_to_db(fact)?;
592            }
593        }
594
595        Ok(decayed)
596    }
597
598    /// Get statistics about the cache
599    pub fn stats(&self) -> CacheStats {
600        let mut by_category: HashMap<PersonalFactCategory, u32> = HashMap::new();
601        let mut total_confidence = 0.0f32;
602        let mut count = 0u32;
603        let mut local_only_count = 0u32;
604
605        for fact in self.facts.values().filter(|f| !f.deleted) {
606            *by_category.entry(fact.category).or_insert(0) += 1;
607            total_confidence += fact.confidence;
608            count += 1;
609            if fact.local_only {
610                local_only_count += 1;
611            }
612        }
613
614        CacheStats {
615            total_facts: count,
616            by_category,
617            avg_confidence: if count > 0 {
618                total_confidence / count as f32
619            } else {
620                0.0
621            },
622            local_only_facts: local_only_count,
623            pending_submissions: self.pending_submissions.len(),
624            pending_feedback: self.pending_feedback.len(),
625            last_sync: self.last_sync,
626        }
627    }
628
629    /// Export all facts as JSON (for /profile export)
630    pub fn export_json(&self) -> Result<String> {
631        let facts: Vec<&PersonalFact> = self.facts.values().filter(|f| !f.deleted).collect();
632        Ok(serde_json::to_string_pretty(&facts)?)
633    }
634
635    /// Import facts from JSON (for /profile import)
636    pub fn import_json(&mut self, json: &str) -> Result<ImportResult> {
637        let facts: Vec<PersonalFact> = serde_json::from_str(json)?;
638        let mut imported = 0;
639        let mut updated = 0;
640
641        for mut fact in facts {
642            if let Some(existing_id) = self.facts_by_key.get(&fact.key) {
643                // Update existing by key
644                fact.id = existing_id.clone();
645                updated += 1;
646            } else {
647                imported += 1;
648            }
649            self.upsert_fact(fact)?;
650        }
651
652        Ok(ImportResult { imported, updated })
653    }
654}
655
656/// Result of merging facts from server
657#[derive(Debug, Clone)]
658pub struct MergeResult {
659    /// Number of new facts added.
660    pub added: u32,
661    /// Number of existing facts updated.
662    pub updated: u32,
663    /// Number of merge conflicts.
664    pub conflicts: u32,
665}
666
667/// Result of importing facts
668#[derive(Debug, Clone)]
669pub struct ImportResult {
670    /// Number of facts imported.
671    pub imported: u32,
672    /// Number of existing facts updated.
673    pub updated: u32,
674}
675
676/// Statistics about the cache
677#[derive(Debug, Clone)]
678pub struct CacheStats {
679    /// Total number of cached facts.
680    pub total_facts: u32,
681    /// Counts by category.
682    pub by_category: HashMap<PersonalFactCategory, u32>,
683    /// Average confidence score.
684    pub avg_confidence: f32,
685    /// Facts that exist only locally.
686    pub local_only_facts: u32,
687    /// Number of pending fact submissions.
688    pub pending_submissions: usize,
689    /// Number of pending feedback reports.
690    pub pending_feedback: usize,
691    /// Unix timestamp of last sync.
692    pub last_sync: i64,
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698    use crate::knowledge::bks_pks::personal::fact::PersonalFactSource;
699
700    fn create_test_fact(key: &str, value: &str) -> PersonalFact {
701        PersonalFact::new(
702            PersonalFactCategory::Preference,
703            key.to_string(),
704            value.to_string(),
705            None,
706            PersonalFactSource::ExplicitStatement,
707            false,
708        )
709    }
710
711    #[test]
712    fn test_cache_creation() {
713        let cache = PersonalKnowledgeCache::in_memory(100).unwrap();
714        assert_eq!(cache.last_sync, 0);
715        assert_eq!(cache.all_facts().count(), 0);
716    }
717
718    #[test]
719    fn test_add_and_get_fact() {
720        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
721        let fact = create_test_fact("language", "Rust");
722
723        let id = fact.id.clone();
724        cache.add_fact(fact).unwrap();
725
726        let retrieved = cache.get_fact(&id).unwrap();
727        assert_eq!(retrieved.value, "Rust");
728    }
729
730    #[test]
731    fn test_get_by_key() {
732        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
733        cache
734            .add_fact(create_test_fact("language", "Rust"))
735            .unwrap();
736
737        let retrieved = cache.get_fact_by_key("language").unwrap();
738        assert_eq!(retrieved.value, "Rust");
739    }
740
741    #[test]
742    fn test_upsert_fact() {
743        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
744
745        // Add initial fact
746        cache
747            .upsert_fact(create_test_fact("language", "Python"))
748            .unwrap();
749        assert_eq!(cache.get_fact_by_key("language").unwrap().value, "Python");
750
751        // Upsert with same key should update
752        cache
753            .upsert_fact(create_test_fact("language", "Rust"))
754            .unwrap();
755        let fact = cache.get_fact_by_key("language").unwrap();
756        assert_eq!(fact.value, "Rust");
757        assert_eq!(fact.reinforcements, 1); // Should be incremented
758    }
759
760    #[test]
761    fn test_facts_by_category() {
762        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
763
764        cache.add_fact(create_test_fact("lang", "Rust")).unwrap();
765
766        let mut identity_fact = create_test_fact("name", "John");
767        identity_fact.category = PersonalFactCategory::Identity;
768        cache.add_fact(identity_fact).unwrap();
769
770        let pref_facts = cache.facts_by_category(PersonalFactCategory::Preference);
771        assert_eq!(pref_facts.len(), 1);
772
773        let id_facts = cache.facts_by_category(PersonalFactCategory::Identity);
774        assert_eq!(id_facts.len(), 1);
775    }
776
777    #[test]
778    fn test_search_facts() {
779        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
780
781        cache
782            .add_fact(create_test_fact("language", "Rust"))
783            .unwrap();
784        cache
785            .add_fact(create_test_fact("framework", "Actix"))
786            .unwrap();
787
788        let results = cache.search_facts("rust");
789        assert_eq!(results.len(), 1);
790        assert_eq!(results[0].key, "language");
791    }
792
793    #[test]
794    fn test_local_only_facts() {
795        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
796
797        let mut local_fact = create_test_fact("secret", "value");
798        local_fact.local_only = true;
799        cache.add_fact(local_fact.clone()).unwrap();
800
801        // Should not be able to queue for sync
802        assert!(!cache.queue_submission(local_fact).unwrap());
803
804        // Should not be in syncable facts
805        assert!(cache.get_syncable_facts().is_empty());
806    }
807
808    #[test]
809    fn test_export_import_json() {
810        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
811
812        cache.add_fact(create_test_fact("lang", "Rust")).unwrap();
813        cache
814            .add_fact(create_test_fact("editor", "VSCode"))
815            .unwrap();
816
817        let json = cache.export_json().unwrap();
818
819        // Create new cache and import
820        let mut new_cache = PersonalKnowledgeCache::in_memory(100).unwrap();
821        let result = new_cache.import_json(&json).unwrap();
822
823        assert_eq!(result.imported, 2);
824        assert_eq!(result.updated, 0);
825        assert_eq!(new_cache.all_facts().count(), 2);
826    }
827
828    #[test]
829    fn test_stats() {
830        let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
831
832        cache.add_fact(create_test_fact("lang", "Rust")).unwrap();
833
834        let mut local_fact = create_test_fact("secret", "value");
835        local_fact.local_only = true;
836        cache.add_fact(local_fact).unwrap();
837
838        let stats = cache.stats();
839        assert_eq!(stats.total_facts, 2);
840        assert_eq!(stats.local_only_facts, 1);
841    }
842}