Skip to main content

oxios_kernel/memory/
auto_memory_bridge.rs

1//! Auto-memory bridge: synchronization between external memory systems and Oxios.
2//!
3//! Bridges Oxios MemoryManager with Claude Code's MEMORY.md format and
4//! similar external memory stores. Supports bidirectional sync:
5//!
6//! - **to-auto**: Export Oxios patterns/insights → external MEMORY.md format
7//! - **from-auto**: Import external memories → Oxios MemoryStore
8//! - **bidirectional**: Full two-way synchronization
9//!
10//! The bridge converts between Oxios's structured memory entries and
11//! free-form markdown memory formats used by external tools.
12
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use anyhow::Result;
17use chrono::Utc;
18use serde::{Deserialize, Serialize};
19
20use super::{MemoryEntry, MemoryManager, MemoryType};
21
22// ---------------------------------------------------------------------------
23// Types
24// ---------------------------------------------------------------------------
25
26/// Direction of memory synchronization.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum SyncDirection {
30    /// Export Oxios → external format.
31    ToAuto,
32    /// Import external → Oxios.
33    FromAuto,
34    /// Two-way sync.
35    Bidirectional,
36}
37
38/// Category of an imported memory insight.
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum InsightCategory {
42    /// Project-level patterns and conventions.
43    ProjectPatterns,
44    /// Debugging strategies and fixes.
45    Debugging,
46    /// Architecture decisions.
47    Architecture,
48    /// Performance observations.
49    Performance,
50    /// Security-related insights.
51    Security,
52    /// General knowledge.
53    General,
54}
55
56impl InsightCategory {
57    /// Parse from a string.
58    pub fn from_str_loose(s: &str) -> Self {
59        match s.to_lowercase().as_str() {
60            "project-patterns" | "project_patterns" | "patterns" => {
61                InsightCategory::ProjectPatterns
62            }
63            "debugging" | "debug" => InsightCategory::Debugging,
64            "architecture" | "arch" => InsightCategory::Architecture,
65            "performance" | "perf" => InsightCategory::Performance,
66            "security" | "sec" => InsightCategory::Security,
67            _ => InsightCategory::General,
68        }
69    }
70
71    /// Convert to a tag string.
72    pub fn to_tag(&self) -> &'static str {
73        match self {
74            InsightCategory::ProjectPatterns => "project-patterns",
75            InsightCategory::Debugging => "debugging",
76            InsightCategory::Architecture => "architecture",
77            InsightCategory::Performance => "performance",
78            InsightCategory::Security => "security",
79            InsightCategory::General => "general",
80        }
81    }
82}
83
84/// A single imported memory insight from an external system.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct MemoryInsight {
87    /// Category of the insight.
88    pub category: InsightCategory,
89    /// Brief summary (1 line).
90    pub summary: String,
91    /// Optional detailed content.
92    pub detail: Option<String>,
93    /// Source identifier (e.g., "claude-code", "user").
94    pub source: String,
95    /// Confidence score (0.0 - 1.0).
96    pub confidence: f32,
97}
98
99/// Result of importing memories from an external system.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct ImportResult {
102    /// Number of insights successfully imported.
103    pub imported: usize,
104    /// Number of duplicates skipped.
105    pub skipped_duplicates: usize,
106    /// Number of failed imports.
107    pub failed: usize,
108    /// Error messages for failed imports.
109    pub errors: Vec<String>,
110}
111
112/// Result of exporting memories to an external system.
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct ExportResult {
115    /// Number of patterns exported.
116    pub exported: usize,
117    /// Number of categories created/updated.
118    pub categories_updated: usize,
119}
120
121/// Result of a bidirectional sync operation.
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct SyncResult {
124    /// Import results.
125    pub import: ImportResult,
126    /// Export results.
127    pub export: ExportResult,
128}
129
130/// A guidance pattern for export to external format.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct GuidancePattern {
133    /// Pattern identifier.
134    pub id: String,
135    /// Category.
136    pub category: InsightCategory,
137    /// Brief description.
138    pub description: String,
139    /// Confidence/importance.
140    pub confidence: f32,
141    /// Usage count.
142    pub usage_count: u32,
143}
144
145// ---------------------------------------------------------------------------
146// AutoMemoryBridge
147// ---------------------------------------------------------------------------
148
149/// Bridge between Oxios memory and external memory systems.
150///
151/// Bridges Oxios MemoryManager (agent session memory) and optional
152/// KnowledgeBase (markdown knowledge base) with external MEMORY.md
153/// files. Supports bidirectional sync with Claude Code and similar
154/// external tools.
155///
156/// **RFC-003: KnowledgeBase is the single source of truth for markdown.
157/// MemoryManager stores agent session memory. The bridge can sync from
158/// either source or both.**
159pub struct AutoMemoryBridge {
160    /// Directory containing external memory files.
161    auto_memory_dir: PathBuf,
162    /// Oxios memory manager (agent session memory).
163    oxios_memory: std::sync::Arc<MemoryManager>,
164    /// Optional markdown knowledge base (global, not per-Space).
165    knowledge_base: Option<std::sync::Arc<oxios_markdown::KnowledgeBase>>,
166}
167
168impl AutoMemoryBridge {
169    /// Create a new bridge.
170    ///
171    /// # Arguments
172    /// * `auto_memory_dir` - Path to directory containing MEMORY.md files
173    /// * `oxios_memory` - Oxios MemoryManager instance
174    pub fn new(auto_memory_dir: PathBuf, oxios_memory: std::sync::Arc<MemoryManager>) -> Self {
175        Self {
176            auto_memory_dir,
177            oxios_memory,
178            knowledge_base: None,
179        }
180    }
181
182    /// Set the optional markdown knowledge base.
183    ///
184    /// When set, `export_knowledge_to_auto()` can read directly from
185    /// `.md` files instead of relying on MemoryManager entries.
186    pub fn with_knowledge_base(
187        mut self,
188        kb: std::sync::Arc<oxios_markdown::KnowledgeBase>,
189    ) -> Self {
190        self.knowledge_base = Some(kb);
191        self
192    }
193
194    /// Import memories from external format into Oxios.
195    ///
196    /// Reads MEMORY.md files from the auto_memory_dir, parses them into
197    /// structured insights, and stores them via MemoryManager.
198    pub async fn import_from_auto(&self) -> Result<ImportResult> {
199        let mut result = ImportResult::default();
200
201        // Find all MEMORY.md files
202        let memory_files = self.find_memory_files()?;
203
204        for file_path in &memory_files {
205            match self.import_file(file_path).await {
206                Ok(file_result) => {
207                    result.imported += file_result.imported;
208                    result.skipped_duplicates += file_result.skipped_duplicates;
209                    result.failed += file_result.failed;
210                    result.errors.extend(file_result.errors);
211                }
212                Err(e) => {
213                    result.failed += 1;
214                    result
215                        .errors
216                        .push(format!("{}: {}", file_path.display(), e));
217                }
218            }
219        }
220
221        Ok(result)
222    }
223
224    /// Export Oxios patterns to external MEMORY.md format.
225    ///
226    /// Reads patterns from Oxios memory and writes them as structured
227    /// markdown files in the auto_memory_dir.
228    pub async fn export_to_auto(&self, patterns: &[GuidancePattern]) -> Result<ExportResult> {
229        let mut result = ExportResult::default();
230
231        // Ensure output directory exists
232        tokio::fs::create_dir_all(&self.auto_memory_dir).await?;
233
234        // Group patterns by category
235        let mut by_category: HashMap<InsightCategory, Vec<&GuidancePattern>> = HashMap::new();
236        for pattern in patterns {
237            by_category
238                .entry(pattern.category.clone())
239                .or_default()
240                .push(pattern);
241        }
242
243        // Write each category to its own file
244        for (category, cat_patterns) in &by_category {
245            let filename = match category {
246                InsightCategory::ProjectPatterns => "patterns.md",
247                InsightCategory::Debugging => "debugging.md",
248                InsightCategory::Architecture => "architecture.md",
249                InsightCategory::Performance => "performance.md",
250                InsightCategory::Security => "security.md",
251                InsightCategory::General => "general.md",
252            };
253
254            let content = self.format_patterns_md(cat_patterns);
255            let path = self.auto_memory_dir.join(filename);
256
257            tokio::fs::write(&path, &content).await?;
258            result.categories_updated += 1;
259        }
260
261        // Write main MEMORY.md with all patterns sorted by confidence
262        let mut all_patterns: Vec<&GuidancePattern> = patterns.iter().collect();
263        all_patterns.sort_by(|a, b| {
264            b.confidence
265                .partial_cmp(&a.confidence)
266                .unwrap_or(std::cmp::Ordering::Equal)
267        });
268
269        let main_content = self.format_main_md(&all_patterns);
270        let main_path = self.auto_memory_dir.join("MEMORY.md");
271        tokio::fs::write(&main_path, &main_content).await?;
272
273        result.exported = patterns.len();
274        result.categories_updated += 1; // MEMORY.md itself
275        Ok(result)
276    }
277
278    /// Perform a bidirectional sync.
279    pub async fn sync_session(&self, direction: SyncDirection) -> Result<SyncResult> {
280        let mut sync_result = SyncResult::default();
281
282        match direction {
283            SyncDirection::FromAuto => {
284                sync_result.import = self.import_from_auto().await?;
285            }
286            SyncDirection::ToAuto => {
287                sync_result.export = self.export_knowledge_to_auto().await?;
288            }
289            SyncDirection::Bidirectional => {
290                sync_result.import = self.import_from_auto().await?;
291                sync_result.export = self.export_knowledge_to_auto().await?;
292            }
293        }
294
295        Ok(sync_result)
296    }
297
298    /// Returns the auto-memory directory path.
299    pub fn auto_memory_dir(&self) -> &Path {
300        &self.auto_memory_dir
301    }
302
303    /// Export all knowledge memories from Oxios to external format.
304    ///
305    /// Reads from MemoryManager (primary) or falls back to KnowledgeBase
306    /// `.md` files when `knowledge_base` is set and MemoryManager has no
307    /// `MemoryType::Knowledge` entries.
308    async fn export_knowledge_to_auto(&self) -> Result<ExportResult> {
309        // Try MemoryManager first (primary source)
310        let entries = self
311            .oxios_memory
312            .list(MemoryType::Knowledge, 1000)
313            .await
314            .unwrap_or_default();
315
316        if !entries.is_empty() {
317            let patterns: Vec<GuidancePattern> = entries
318                .iter()
319                .map(|e| GuidancePattern {
320                    id: e.id.clone(),
321                    category: e
322                        .tags
323                        .first()
324                        .map(|t| InsightCategory::from_str_loose(t))
325                        .unwrap_or(InsightCategory::General),
326                    description: e.content.clone(),
327                    confidence: e.importance,
328                    usage_count: e.access_count,
329                })
330                .collect();
331            return self.export_to_auto(&patterns).await;
332        }
333
334        // Fall back to KnowledgeBase .md files (RFC-003)
335        if let Some(kb) = &self.knowledge_base {
336            let entries = kb.index_all()?;
337            if entries > 0 {
338                let patterns = kb
339                    .note_tree("/")
340                    .unwrap_or_default()
341                    .into_iter()
342                    .filter(|e| !e.is_dir && e.name.ends_with(".md"))
343                    .map(|e| {
344                        let path = if e.parent_dir == "/" || e.parent_dir.is_empty() {
345                            e.name.clone()
346                        } else {
347                            format!("{}/{}", e.parent_dir, e.name)
348                        };
349                        let content = kb.note_read(&path).ok().flatten().unwrap_or_default();
350                        let headings = kb.extract_headings(&content);
351                        let tag = headings.first().map(|s| s.as_str()).unwrap_or("general");
352                        GuidancePattern {
353                            id: format!("note-{}", path.replace('/', "-").trim_end_matches(".md")),
354                            category: InsightCategory::from_str_loose(tag),
355                            description: content.chars().take(300).collect(),
356                            confidence: 0.6,
357                            usage_count: 0,
358                        }
359                    })
360                    .collect::<Vec<_>>();
361                if !patterns.is_empty() {
362                    return self.export_to_auto(&patterns).await;
363                }
364            }
365        }
366
367        // No entries from either source — export empty result
368        Ok(ExportResult::default())
369    }
370}
371
372// ---------------------------------------------------------------------------
373// Private helpers
374// ---------------------------------------------------------------------------
375
376impl AutoMemoryBridge {
377    /// Find all MEMORY.md files in the auto_memory_dir.
378    fn find_memory_files(&self) -> Result<Vec<PathBuf>> {
379        let mut files = Vec::new();
380
381        if self.auto_memory_dir.exists() {
382            // Look for MEMORY.md directly
383            let main = self.auto_memory_dir.join("MEMORY.md");
384            if main.exists() {
385                files.push(main);
386            }
387
388            // Look for topic files
389            for topic in &[
390                "patterns.md",
391                "debugging.md",
392                "architecture.md",
393                "performance.md",
394                "security.md",
395                "general.md",
396            ] {
397                let path = self.auto_memory_dir.join(topic);
398                if path.exists() {
399                    files.push(path);
400                }
401            }
402
403            // Look for *.md files in subdirectories
404            if let Ok(entries) = std::fs::read_dir(&self.auto_memory_dir) {
405                for entry in entries.flatten() {
406                    let path = entry.path();
407                    if path.extension().is_some_and(|ext| ext == "md") {
408                        let name = path.file_name().unwrap_or_default().to_string_lossy();
409                        // Skip already-added files
410                        if ![
411                            "MEMORY.md",
412                            "patterns.md",
413                            "debugging.md",
414                            "architecture.md",
415                            "performance.md",
416                            "security.md",
417                            "general.md",
418                        ]
419                        .contains(&name.as_ref())
420                        {
421                            files.push(path);
422                        }
423                    }
424                }
425            }
426        }
427
428        Ok(files)
429    }
430
431    /// Import a single markdown file into Oxios memory.
432    async fn import_file(&self, path: &Path) -> Result<ImportResult> {
433        let content = tokio::fs::read_to_string(path).await?;
434        let insights = self.parse_markdown_insights(&content);
435        let mut result = ImportResult::default();
436
437        for insight in &insights {
438            // Check for duplicates
439            if self.oxios_memory.is_duplicate(&insight.summary).await {
440                result.skipped_duplicates += 1;
441                continue;
442            }
443
444            let entry = MemoryEntry {
445                id: format!(
446                    "auto-{}-{}",
447                    insight.source,
448                    chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
449                ),
450                memory_type: MemoryType::Knowledge,
451                content: match &insight.detail {
452                    Some(d) => format!("{}\n\n{}", insight.summary, d),
453                    None => insight.summary.clone(),
454                },
455                source: insight.source.clone(),
456                session_id: None,
457                tags: vec![insight.category.to_tag().to_string()],
458                importance: insight.confidence,
459                created_at: Utc::now(),
460                accessed_at: Utc::now(),
461                access_count: 0,
462            };
463
464            match self.oxios_memory.remember(entry).await {
465                Ok(_) => result.imported += 1,
466                Err(e) => {
467                    result.failed += 1;
468                    result.errors.push(e.to_string());
469                    tracing::warn!(error = %e, "Failed to import memory insight");
470                }
471            }
472        }
473
474        Ok(result)
475    }
476
477    /// Parse insights from markdown content.
478    ///
479    /// Supports multiple formats:
480    /// - List items: `- **Category**: Description`
481    /// - Headers: `## Category` followed by bullet points
482    /// - Free-form text split into chunks
483    fn parse_markdown_insights(&self, content: &str) -> Vec<MemoryInsight> {
484        let mut insights = Vec::new();
485        let mut current_category = InsightCategory::General;
486
487        for line in content.lines() {
488            let trimmed = line.trim();
489
490            // Skip empty lines
491            if trimmed.is_empty() {
492                continue;
493            }
494
495            // Detect category headers: ## Category or # Category
496            if trimmed.starts_with('#') {
497                let header = trimmed.trim_start_matches('#').trim().to_lowercase();
498                current_category = InsightCategory::from_str_loose(&header);
499                continue;
500            }
501
502            // Parse bullet list items: - **Category**: Description
503            if trimmed.starts_with('-') || trimmed.starts_with('*') {
504                // Strip the leading list marker and whitespace, preserving ** bold markers
505                let item = if trimmed.starts_with('-') {
506                    trimmed.trim_start_matches('-').trim_start()
507                } else {
508                    trimmed.trim_start_matches('*').trim_start()
509                };
510
511                // Check for bold category prefix: **Category**:
512                if let Some(rest) = Self::extract_bold_category(item) {
513                    let (cat_name, description) = rest;
514                    let category = InsightCategory::from_str_loose(cat_name);
515
516                    // Split on colon for detail
517                    let (summary, detail) = if let Some(pos) = description.find(':') {
518                        let s = description[..pos].trim();
519                        let d = description[pos + 1..].trim();
520                        (
521                            s.to_string(),
522                            if d.is_empty() {
523                                None
524                            } else {
525                                Some(d.to_string())
526                            },
527                        )
528                    } else {
529                        (description.to_string(), None)
530                    };
531
532                    if !summary.is_empty() {
533                        insights.push(MemoryInsight {
534                            category,
535                            summary,
536                            detail,
537                            source: "auto-import".to_string(),
538                            confidence: 0.7,
539                        });
540                    }
541                } else if !item.is_empty() {
542                    // Plain bullet point
543                    insights.push(MemoryInsight {
544                        category: current_category.clone(),
545                        summary: item.to_string(),
546                        detail: None,
547                        source: "auto-import".to_string(),
548                        confidence: 0.6,
549                    });
550                }
551            }
552        }
553
554        // If no structured content found, treat the whole content as a single insight
555        if insights.is_empty() && !content.trim().is_empty() {
556            let summary: String = content
557                .lines()
558                .take(3)
559                .collect::<Vec<_>>()
560                .join(" ")
561                .chars()
562                .take(200)
563                .collect();
564
565            if !summary.trim().is_empty() {
566                insights.push(MemoryInsight {
567                    category: InsightCategory::General,
568                    summary,
569                    detail: Some(content.to_string()),
570                    source: "auto-import".to_string(),
571                    confidence: 0.5,
572                });
573            }
574        }
575
576        insights
577    }
578
579    /// Extract a bold category prefix from a markdown item.
580    ///
581    /// Returns Some((category, rest)) if the item starts with **Category**:
582    fn extract_bold_category(item: &str) -> Option<(&str, &str)> {
583        if !item.starts_with("**") {
584            return None;
585        }
586
587        let end = item[2..].find("**")?;
588        let category = &item[2..2 + end];
589        let rest = item[2 + end + 2..].trim_start_matches([' ', ':']);
590
591        Some((category, rest))
592    }
593
594    /// Format patterns as a category-specific markdown file.
595    fn format_patterns_md(&self, patterns: &[&GuidancePattern]) -> String {
596        let mut md = String::new();
597        md.push_str("# Memory Insights\n\n");
598
599        for pattern in patterns {
600            let confidence_bar = format_confidence_bar(pattern.confidence);
601            md.push_str(&format!(
602                "- **{}**: {} [{}]\n",
603                pattern.category.to_tag(),
604                pattern.description,
605                confidence_bar,
606            ));
607        }
608
609        md.push('\n');
610        md
611    }
612
613    /// Format all patterns as the main MEMORY.md file.
614    fn format_main_md(&self, patterns: &[&GuidancePattern]) -> String {
615        let mut md = String::new();
616        md.push_str("# Oxios Memory\n\n");
617        md.push_str(&format!(
618            "Auto-generated at {}\n\n",
619            Utc::now().to_rfc3339()
620        ));
621        md.push_str("## Insights\n\n");
622
623        for pattern in patterns {
624            let confidence_pct = (pattern.confidence * 100.0) as u8;
625            md.push_str(&format!(
626                "- **{}** [{}%]: {}\n",
627                pattern.category.to_tag(),
628                confidence_pct,
629                pattern.description,
630            ));
631        }
632
633        md.push('\n');
634        md
635    }
636}
637
638/// Format a confidence value as a visual bar.
639fn format_confidence_bar(confidence: f32) -> String {
640    let bars = (confidence * 5.0).round() as usize;
641    let bars = bars.min(5);
642    let filled: String = "█".repeat(bars);
643    let empty: String = "░".repeat(5 - bars);
644    format!("{}{} {:.0}%", filled, empty, confidence * 100.0)
645}
646
647// ---------------------------------------------------------------------------
648// Tests
649// ---------------------------------------------------------------------------
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use std::sync::Arc;
655
656    fn make_bridge(dir: &Path) -> AutoMemoryBridge {
657        let store = Arc::new(crate::state_store::StateStore::new(dir.join("state")).unwrap());
658        let memory = Arc::new(MemoryManager::new(store));
659        AutoMemoryBridge::new(dir.join("auto"), memory).with_knowledge_base(Arc::new(
660            oxios_markdown::KnowledgeBase::new(dir.join("kb")).unwrap(),
661        ))
662    }
663
664    #[test]
665    fn test_parse_bold_category() {
666        let result =
667            AutoMemoryBridge::extract_bold_category("**Debugging**: Use trace-level logging");
668        assert!(result.is_some());
669        let (cat, rest) = result.unwrap();
670        assert_eq!(cat, "Debugging");
671        assert_eq!(rest, "Use trace-level logging");
672    }
673
674    #[test]
675    fn test_parse_bold_category_no_colon() {
676        let result = AutoMemoryBridge::extract_bold_category("**Security** important rule");
677        assert!(result.is_some());
678        let (cat, rest) = result.unwrap();
679        assert_eq!(cat, "Security");
680        assert_eq!(rest, "important rule");
681    }
682
683    #[test]
684    fn test_parse_no_bold() {
685        let result = AutoMemoryBridge::extract_bold_category("Just a plain item");
686        assert!(result.is_none());
687    }
688
689    #[test]
690    fn test_parse_markdown_insights() {
691        let temp_dir = tempfile::tempdir().unwrap();
692        let bridge = make_bridge(temp_dir.path());
693
694        let md = r#"# Project Patterns
695
696- **Debugging**: Use trace-level logging for async tasks
697- **Architecture**: Follow the kernel module pattern
698- **Performance**: Batch embeddings when possible
699
700## Security
701
702- Always validate input at the boundary
703- Use RBAC for multi-agent access control
704"#;
705
706        let insights = bridge.parse_markdown_insights(md);
707        assert!(!insights.is_empty());
708
709        // Should have parsed the bullet points
710        let debugging = insights
711            .iter()
712            .find(|i| i.category == InsightCategory::Debugging);
713        assert!(debugging.is_some());
714
715        let security = insights
716            .iter()
717            .find(|i| i.category == InsightCategory::Security);
718        assert!(security.is_some());
719    }
720
721    #[test]
722    fn test_parse_empty_markdown() {
723        let temp_dir = tempfile::tempdir().unwrap();
724        let bridge = make_bridge(temp_dir.path());
725
726        let insights = bridge.parse_markdown_insights("");
727        assert!(insights.is_empty());
728    }
729
730    #[test]
731    fn test_parse_plain_text_as_single_insight() {
732        let temp_dir = tempfile::tempdir().unwrap();
733        let bridge = make_bridge(temp_dir.path());
734
735        let text = "This is a plain text memory entry without markdown formatting.";
736        let insights = bridge.parse_markdown_insights(text);
737        assert_eq!(insights.len(), 1);
738        assert_eq!(insights[0].category, InsightCategory::General);
739    }
740
741    #[test]
742    fn test_insight_category_parsing() {
743        assert_eq!(
744            InsightCategory::from_str_loose("patterns"),
745            InsightCategory::ProjectPatterns
746        );
747        assert_eq!(
748            InsightCategory::from_str_loose("debug"),
749            InsightCategory::Debugging
750        );
751        assert_eq!(
752            InsightCategory::from_str_loose("arch"),
753            InsightCategory::Architecture
754        );
755        assert_eq!(
756            InsightCategory::from_str_loose("perf"),
757            InsightCategory::Performance
758        );
759        assert_eq!(
760            InsightCategory::from_str_loose("sec"),
761            InsightCategory::Security
762        );
763        assert_eq!(
764            InsightCategory::from_str_loose("unknown"),
765            InsightCategory::General
766        );
767    }
768
769    #[test]
770    fn test_confidence_bar() {
771        let bar = format_confidence_bar(0.8);
772        assert!(bar.contains("80%"));
773
774        let bar_low = format_confidence_bar(0.2);
775        assert!(bar_low.contains("20%"));
776    }
777
778    #[tokio::test]
779    async fn test_import_from_empty_dir() {
780        let temp_dir = tempfile::tempdir().unwrap();
781        let bridge = make_bridge(temp_dir.path());
782
783        // No files to import
784        let result = bridge.import_from_auto().await.unwrap();
785        assert_eq!(result.imported, 0);
786        assert_eq!(result.failed, 0);
787    }
788
789    #[tokio::test]
790    async fn test_import_from_file() {
791        let temp_dir = tempfile::tempdir().unwrap();
792        let auto_dir = temp_dir.path().join("auto");
793        tokio::fs::create_dir_all(&auto_dir).await.unwrap();
794
795        // Create a MEMORY.md file
796        let md = r#"- **Debugging**: Use println! for quick debugging
797- **Architecture**: Keep modules small and focused
798"#;
799        tokio::fs::write(auto_dir.join("MEMORY.md"), md)
800            .await
801            .unwrap();
802
803        let bridge = make_bridge(temp_dir.path());
804        let result = bridge.import_from_auto().await.unwrap();
805        assert!(result.imported >= 1, "Should import at least 1 insight");
806    }
807
808    #[tokio::test]
809    async fn test_export_to_auto() {
810        let temp_dir = tempfile::tempdir().unwrap();
811        let bridge = make_bridge(temp_dir.path());
812
813        let patterns = vec![
814            GuidancePattern {
815                id: "p1".to_string(),
816                category: InsightCategory::Debugging,
817                description: "Always check error chains".to_string(),
818                confidence: 0.9,
819                usage_count: 5,
820            },
821            GuidancePattern {
822                id: "p2".to_string(),
823                category: InsightCategory::Architecture,
824                description: "Use actor model for concurrency".to_string(),
825                confidence: 0.7,
826                usage_count: 3,
827            },
828        ];
829
830        let result = bridge.export_to_auto(&patterns).await.unwrap();
831        assert_eq!(result.exported, 2);
832        assert!(result.categories_updated >= 2);
833
834        // Verify files were created
835        assert!(bridge.auto_memory_dir.join("MEMORY.md").exists());
836        assert!(bridge.auto_memory_dir.join("debugging.md").exists());
837        assert!(bridge.auto_memory_dir.join("architecture.md").exists());
838
839        // Verify content
840        let main = tokio::fs::read_to_string(bridge.auto_memory_dir.join("MEMORY.md"))
841            .await
842            .unwrap();
843        assert!(main.contains("Oxios Memory"));
844        assert!(main.contains("Always check error chains"));
845        assert!(main.contains("Use actor model for concurrency"));
846    }
847
848    #[tokio::test]
849    async fn test_bidirectional_sync() {
850        let temp_dir = tempfile::tempdir().unwrap();
851        let auto_dir = temp_dir.path().join("auto");
852        tokio::fs::create_dir_all(&auto_dir).await.unwrap();
853
854        // Create an external memory file
855        tokio::fs::write(
856            auto_dir.join("MEMORY.md"),
857            "- **Debugging**: Test insight for sync",
858        )
859        .await
860        .unwrap();
861
862        let bridge = make_bridge(temp_dir.path());
863        let result = bridge
864            .sync_session(SyncDirection::Bidirectional)
865            .await
866            .unwrap();
867
868        // Should have imported the insight
869        assert!(result.import.imported >= 1 || result.import.skipped_duplicates > 0);
870
871        // Should have exported current state (usize, always >= 0)
872    }
873
874    #[test]
875    fn test_sync_direction_serialization() {
876        let dir = SyncDirection::ToAuto;
877        let json = serde_json::to_string(&dir).unwrap();
878        assert_eq!(json, "\"to_auto\"");
879
880        let parsed: SyncDirection = serde_json::from_str(&json).unwrap();
881        assert_eq!(parsed, SyncDirection::ToAuto);
882    }
883}