1use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use anyhow::Result;
17use chrono::Utc;
18use serde::{Deserialize, Serialize};
19
20use crate::memory::manager::MemoryManager;
21use crate::memory::types::{MemoryEntry, MemoryType};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum SyncDirection {
31 ToAuto,
33 FromAuto,
35 Bidirectional,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum InsightCategory {
43 ProjectPatterns,
45 Debugging,
47 Architecture,
49 Performance,
51 Security,
53 General,
55}
56
57impl InsightCategory {
58 pub fn from_str_loose(s: &str) -> Self {
60 match s.to_lowercase().as_str() {
61 "project-patterns" | "project_patterns" | "patterns" => {
62 InsightCategory::ProjectPatterns
63 }
64 "debugging" | "debug" => InsightCategory::Debugging,
65 "architecture" | "arch" => InsightCategory::Architecture,
66 "performance" | "perf" => InsightCategory::Performance,
67 "security" | "sec" => InsightCategory::Security,
68 _ => InsightCategory::General,
69 }
70 }
71
72 pub fn to_tag(&self) -> &'static str {
74 match self {
75 InsightCategory::ProjectPatterns => "project-patterns",
76 InsightCategory::Debugging => "debugging",
77 InsightCategory::Architecture => "architecture",
78 InsightCategory::Performance => "performance",
79 InsightCategory::Security => "security",
80 InsightCategory::General => "general",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct MemoryInsight {
88 pub category: InsightCategory,
90 pub summary: String,
92 pub detail: Option<String>,
94 pub source: String,
96 pub confidence: f32,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct ImportResult {
103 pub imported: usize,
105 pub skipped_duplicates: usize,
107 pub failed: usize,
109 pub errors: Vec<String>,
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct ExportResult {
116 pub exported: usize,
118 pub categories_updated: usize,
120}
121
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct SyncResult {
125 pub import: ImportResult,
127 pub export: ExportResult,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct GuidancePattern {
134 pub id: String,
136 pub category: InsightCategory,
138 pub description: String,
140 pub confidence: f32,
142 pub usage_count: u32,
144}
145
146pub struct AutoMemoryBridge {
161 auto_memory_dir: PathBuf,
163 oxios_memory: std::sync::Arc<MemoryManager>,
165 knowledge_base: Option<std::sync::Arc<dyn crate::memory::storage::MarkdownSource>>,
167}
168
169impl AutoMemoryBridge {
170 pub fn new(auto_memory_dir: PathBuf, oxios_memory: std::sync::Arc<MemoryManager>) -> Self {
176 Self {
177 auto_memory_dir,
178 oxios_memory,
179 knowledge_base: None,
180 }
181 }
182
183 pub fn with_knowledge_base(
188 mut self,
189 kb: std::sync::Arc<dyn crate::memory::storage::MarkdownSource>,
190 ) -> Self {
191 self.knowledge_base = Some(kb);
192 self
193 }
194
195 pub async fn import_from_auto(&self) -> Result<ImportResult> {
200 let mut result = ImportResult::default();
201
202 let memory_files = self.find_memory_files()?;
204
205 for file_path in &memory_files {
206 match self.import_file(file_path).await {
207 Ok(file_result) => {
208 result.imported += file_result.imported;
209 result.skipped_duplicates += file_result.skipped_duplicates;
210 result.failed += file_result.failed;
211 result.errors.extend(file_result.errors);
212 }
213 Err(e) => {
214 result.failed += 1;
215 result
216 .errors
217 .push(format!("{}: {}", file_path.display(), e));
218 }
219 }
220 }
221
222 Ok(result)
223 }
224
225 pub async fn export_to_auto(&self, patterns: &[GuidancePattern]) -> Result<ExportResult> {
230 let mut result = ExportResult::default();
231
232 tokio::fs::create_dir_all(&self.auto_memory_dir).await?;
234
235 let mut by_category: HashMap<InsightCategory, Vec<&GuidancePattern>> = HashMap::new();
237 for pattern in patterns {
238 by_category
239 .entry(pattern.category.clone())
240 .or_default()
241 .push(pattern);
242 }
243
244 for (category, cat_patterns) in &by_category {
246 let filename = match category {
247 InsightCategory::ProjectPatterns => "patterns.md",
248 InsightCategory::Debugging => "debugging.md",
249 InsightCategory::Architecture => "architecture.md",
250 InsightCategory::Performance => "performance.md",
251 InsightCategory::Security => "security.md",
252 InsightCategory::General => "general.md",
253 };
254
255 let content = self.format_patterns_md(cat_patterns);
256 let path = self.auto_memory_dir.join(filename);
257
258 tokio::fs::write(&path, &content).await?;
259 result.categories_updated += 1;
260 }
261
262 let mut all_patterns: Vec<&GuidancePattern> = patterns.iter().collect();
264 all_patterns.sort_by(|a, b| {
265 b.confidence
266 .partial_cmp(&a.confidence)
267 .unwrap_or(std::cmp::Ordering::Equal)
268 });
269
270 let main_content = self.format_main_md(&all_patterns);
271 let main_path = self.auto_memory_dir.join("MEMORY.md");
272 tokio::fs::write(&main_path, &main_content).await?;
273
274 result.exported = patterns.len();
275 result.categories_updated += 1; Ok(result)
277 }
278
279 pub async fn sync_session(&self, direction: SyncDirection) -> Result<SyncResult> {
281 let mut sync_result = SyncResult::default();
282
283 match direction {
284 SyncDirection::FromAuto => {
285 sync_result.import = self.import_from_auto().await?;
286 }
287 SyncDirection::ToAuto => {
288 sync_result.export = self.export_knowledge_to_auto().await?;
289 }
290 SyncDirection::Bidirectional => {
291 sync_result.import = self.import_from_auto().await?;
292 sync_result.export = self.export_knowledge_to_auto().await?;
293 }
294 }
295
296 Ok(sync_result)
297 }
298
299 pub fn auto_memory_dir(&self) -> &Path {
301 &self.auto_memory_dir
302 }
303
304 #[cfg(feature = "sqlite-memory")]
309 pub async fn sync_sqlite_to_auto(
310 &self,
311 store: &crate::memory::sqlite::SqliteMemoryStore,
312 ) -> Result<ExportResult> {
313 let rows = store.load_patterns()?;
314 if rows.is_empty() {
315 return Ok(ExportResult::default());
316 }
317
318 let patterns: Vec<GuidancePattern> = rows
319 .iter()
320 .map(|r| GuidancePattern {
321 id: r.id.clone(),
322 category: r
323 .domain
324 .as_deref()
325 .map(InsightCategory::from_str_loose)
326 .unwrap_or(InsightCategory::General),
327 description: format!(
328 "[{}] quality={:.2} uses={}",
329 r.strategy, r.quality, r.use_count
330 ),
331 confidence: r.quality,
332 usage_count: r.use_count,
333 })
334 .collect();
335
336 self.export_to_auto(&patterns).await
337 }
338
339 #[cfg(feature = "sqlite-memory")]
343 pub async fn sync_auto_to_sqlite(
344 &self,
345 store: &crate::memory::sqlite::SqliteMemoryStore,
346 ) -> Result<ImportResult> {
347 let import_result = self.import_from_auto().await?;
348
349 let memory_files = self.find_memory_files()?;
351 for file_path in &memory_files {
352 if let Ok(content) = tokio::fs::read_to_string(file_path).await {
353 let insights = self.parse_insights(&content);
354 for insight in insights {
355 let id = uuid::Uuid::new_v4().to_string();
356 let data = serde_json::to_string(&insight)?;
357 let _ = store.save_pattern(
358 &id,
359 "imported",
360 Some(insight.category.to_tag()),
361 insight.confidence,
362 &data,
363 );
364 }
365 }
366 }
367
368 Ok(import_result)
369 }
370
371 async fn export_knowledge_to_auto(&self) -> Result<ExportResult> {
377 let entries = self
379 .oxios_memory
380 .list(MemoryType::Knowledge, 1000)
381 .await
382 .unwrap_or_default();
383
384 if !entries.is_empty() {
385 let patterns: Vec<GuidancePattern> = entries
386 .iter()
387 .map(|e| GuidancePattern {
388 id: e.id.clone(),
389 category: e
390 .tags
391 .first()
392 .map(|t| InsightCategory::from_str_loose(t))
393 .unwrap_or(InsightCategory::General),
394 description: e.content.clone(),
395 confidence: e.importance,
396 usage_count: e.access_count,
397 })
398 .collect();
399 return self.export_to_auto(&patterns).await;
400 }
401
402 if let Some(kb) = &self.knowledge_base {
404 let entries = kb.index_all()?;
405 if entries > 0 {
406 let patterns = kb
407 .note_tree("/")
408 .unwrap_or_default()
409 .into_iter()
410 .filter(|e| !e.is_dir && e.name.ends_with(".md"))
411 .map(|e| {
412 let path = if e.parent_dir == "/" || e.parent_dir.is_empty() {
413 e.name.clone()
414 } else {
415 format!("{}/{}", e.parent_dir, e.name)
416 };
417 let content = kb.note_read(&path).ok().flatten().unwrap_or_default();
418 let headings = kb.extract_headings(&content);
419 let tag = headings.first().map(|s| s.as_str()).unwrap_or("general");
420 GuidancePattern {
421 id: format!("note-{}", path.replace('/', "-").trim_end_matches(".md")),
422 category: InsightCategory::from_str_loose(tag),
423 description: content.chars().take(300).collect(),
424 confidence: 0.6,
425 usage_count: 0,
426 }
427 })
428 .collect::<Vec<_>>();
429 if !patterns.is_empty() {
430 return self.export_to_auto(&patterns).await;
431 }
432 }
433 }
434
435 Ok(ExportResult::default())
437 }
438}
439
440impl AutoMemoryBridge {
445 #[cfg(feature = "sqlite-memory")]
448 fn parse_insights(&self, content: &str) -> Vec<MemoryInsight> {
449 let mut insights = Vec::new();
450
451 for line in content.lines() {
452 let line = line.trim();
453 if line.is_empty() || line.starts_with('#') {
454 continue;
455 }
456 if line.starts_with("- ") || line.starts_with("* ") {
458 let text = line.trim_start_matches("- ").trim_start_matches("* ");
459 let category = if text.contains("debug") || text.contains("fix") {
460 InsightCategory::Debugging
461 } else if text.contains("arch") || text.contains("design") {
462 InsightCategory::Architecture
463 } else if text.contains("perf") || text.contains("speed") {
464 InsightCategory::Performance
465 } else if text.contains("security") || text.contains("vuln") {
466 InsightCategory::Security
467 } else if text.contains("pattern") || text.contains("convention") {
468 InsightCategory::ProjectPatterns
469 } else {
470 InsightCategory::General
471 };
472 insights.push(MemoryInsight {
473 category,
474 summary: text.to_string(),
475 detail: None,
476 source: "auto-bridge".to_string(),
477 confidence: 0.7,
478 });
479 }
480 }
481
482 insights
483 }
484
485 fn find_memory_files(&self) -> Result<Vec<PathBuf>> {
486 let mut files = Vec::new();
487
488 if self.auto_memory_dir.exists() {
489 let main = self.auto_memory_dir.join("MEMORY.md");
491 if main.exists() {
492 files.push(main);
493 }
494
495 for topic in &[
497 "patterns.md",
498 "debugging.md",
499 "architecture.md",
500 "performance.md",
501 "security.md",
502 "general.md",
503 ] {
504 let path = self.auto_memory_dir.join(topic);
505 if path.exists() {
506 files.push(path);
507 }
508 }
509
510 if let Ok(entries) = std::fs::read_dir(&self.auto_memory_dir) {
512 for entry in entries.flatten() {
513 let path = entry.path();
514 if path.extension().is_some_and(|ext| ext == "md") {
515 let name = path.file_name().unwrap_or_default().to_string_lossy();
516 if ![
518 "MEMORY.md",
519 "patterns.md",
520 "debugging.md",
521 "architecture.md",
522 "performance.md",
523 "security.md",
524 "general.md",
525 ]
526 .contains(&name.as_ref())
527 {
528 files.push(path);
529 }
530 }
531 }
532 }
533 }
534
535 Ok(files)
536 }
537
538 async fn import_file(&self, path: &Path) -> Result<ImportResult> {
540 let content = tokio::fs::read_to_string(path).await?;
541 let insights = self.parse_markdown_insights(&content);
542 let mut result = ImportResult::default();
543
544 for insight in &insights {
545 if self.oxios_memory.is_duplicate(&insight.summary).await {
547 result.skipped_duplicates += 1;
548 continue;
549 }
550
551 let entry = MemoryEntry {
552 id: format!(
553 "auto-{}-{}",
554 insight.source,
555 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
556 ),
557 memory_type: MemoryType::Knowledge,
558 tier: crate::memory::types::MemoryTier::Warm,
559 content: match &insight.detail {
560 Some(d) => format!("{}\n\n{}", insight.summary, d),
561 None => insight.summary.clone(),
562 },
563 content_hash: 0,
564 source: insight.source.clone(),
565 session_id: None,
566 tags: vec![insight.category.to_tag().to_string()],
567 importance: insight.confidence,
568 pinned: false,
569 protection: crate::memory::types::ProtectionLevel::None,
570 auto_classified: false,
571 session_appearances: 0,
572 user_corrected: false,
573 seen_in_sessions: vec![],
574 created_at: Utc::now(),
575 accessed_at: Utc::now(),
576 modified_at: Utc::now(),
577 access_count: 0,
578 decay_score: 1.0,
579 compaction_level: 0,
580 compacted_from: vec![],
581 related_ids: vec![],
582 contradicts: None,
583 };
584
585 match self.oxios_memory.remember(entry).await {
586 Ok(_) => result.imported += 1,
587 Err(e) => {
588 result.failed += 1;
589 result.errors.push(e.to_string());
590 tracing::warn!(error = %e, "Failed to import memory insight");
591 }
592 }
593 }
594
595 Ok(result)
596 }
597
598 fn parse_markdown_insights(&self, content: &str) -> Vec<MemoryInsight> {
605 let mut insights = Vec::new();
606 let mut current_category = InsightCategory::General;
607
608 for line in content.lines() {
609 let trimmed = line.trim();
610
611 if trimmed.is_empty() {
613 continue;
614 }
615
616 if trimmed.starts_with('#') {
618 let header = trimmed.trim_start_matches('#').trim().to_lowercase();
619 current_category = InsightCategory::from_str_loose(&header);
620 continue;
621 }
622
623 if trimmed.starts_with('-') || trimmed.starts_with('*') {
625 let item = if trimmed.starts_with('-') {
627 trimmed.trim_start_matches('-').trim_start()
628 } else {
629 trimmed.trim_start_matches('*').trim_start()
630 };
631
632 if let Some(rest) = Self::extract_bold_category(item) {
634 let (cat_name, description) = rest;
635 let category = InsightCategory::from_str_loose(cat_name);
636
637 let (summary, detail) = if let Some(pos) = description.find(':') {
639 let s = description[..pos].trim();
640 let d = description[pos + 1..].trim();
641 (
642 s.to_string(),
643 if d.is_empty() {
644 None
645 } else {
646 Some(d.to_string())
647 },
648 )
649 } else {
650 (description.to_string(), None)
651 };
652
653 if !summary.is_empty() {
654 insights.push(MemoryInsight {
655 category,
656 summary,
657 detail,
658 source: "auto-import".to_string(),
659 confidence: 0.7,
660 });
661 }
662 } else if !item.is_empty() {
663 insights.push(MemoryInsight {
665 category: current_category.clone(),
666 summary: item.to_string(),
667 detail: None,
668 source: "auto-import".to_string(),
669 confidence: 0.6,
670 });
671 }
672 }
673 }
674
675 if insights.is_empty() && !content.trim().is_empty() {
677 let summary: String = content
678 .lines()
679 .take(3)
680 .collect::<Vec<_>>()
681 .join(" ")
682 .chars()
683 .take(200)
684 .collect();
685
686 if !summary.trim().is_empty() {
687 insights.push(MemoryInsight {
688 category: InsightCategory::General,
689 summary,
690 detail: Some(content.to_string()),
691 source: "auto-import".to_string(),
692 confidence: 0.5,
693 });
694 }
695 }
696
697 insights
698 }
699
700 fn extract_bold_category(item: &str) -> Option<(&str, &str)> {
704 if !item.starts_with("**") {
705 return None;
706 }
707
708 let end = item[2..].find("**")?;
709 let category = &item[2..2 + end];
710 let rest = item[2 + end + 2..].trim_start_matches([' ', ':']);
711
712 Some((category, rest))
713 }
714
715 fn format_patterns_md(&self, patterns: &[&GuidancePattern]) -> String {
717 let mut md = String::new();
718 md.push_str("# Memory Insights\n\n");
719
720 for pattern in patterns {
721 let confidence_bar = format_confidence_bar(pattern.confidence);
722 md.push_str(&format!(
723 "- **{}**: {} [{}]\n",
724 pattern.category.to_tag(),
725 pattern.description,
726 confidence_bar,
727 ));
728 }
729
730 md.push('\n');
731 md
732 }
733
734 fn format_main_md(&self, patterns: &[&GuidancePattern]) -> String {
736 let mut md = String::new();
737 md.push_str("# Oxios Memory\n\n");
738 md.push_str(&format!(
739 "Auto-generated at {}\n\n",
740 Utc::now().to_rfc3339()
741 ));
742 md.push_str("## Insights\n\n");
743
744 for pattern in patterns {
745 let confidence_pct = (pattern.confidence * 100.0) as u8;
746 md.push_str(&format!(
747 "- **{}** [{}%]: {}\n",
748 pattern.category.to_tag(),
749 confidence_pct,
750 pattern.description,
751 ));
752 }
753
754 md.push('\n');
755 md
756 }
757}
758
759fn format_confidence_bar(confidence: f32) -> String {
761 let bars = (confidence * 5.0).round() as usize;
762 let bars = bars.min(5);
763 let filled: String = "█".repeat(bars);
764 let empty: String = "░".repeat(5 - bars);
765 format!("{}{} {:.0}%", filled, empty, confidence * 100.0)
766}
767
768#[cfg(test)]
773mod tests {
774 use super::*;
775 use std::sync::Arc;
776
777 fn make_bridge(dir: &Path) -> AutoMemoryBridge {
778 let storage: Arc<dyn crate::memory::storage::MemoryStorage> =
779 Arc::new(crate::memory::test_support::InMemoryStorage::default());
780 let memory = Arc::new(MemoryManager::new(storage));
781 AutoMemoryBridge::new(dir.join("auto"), memory)
782 }
783
784 #[test]
785 fn test_parse_bold_category() {
786 let result =
787 AutoMemoryBridge::extract_bold_category("**Debugging**: Use trace-level logging");
788 assert!(result.is_some());
789 let (cat, rest) = result.unwrap();
790 assert_eq!(cat, "Debugging");
791 assert_eq!(rest, "Use trace-level logging");
792 }
793
794 #[test]
795 fn test_parse_bold_category_no_colon() {
796 let result = AutoMemoryBridge::extract_bold_category("**Security** important rule");
797 assert!(result.is_some());
798 let (cat, rest) = result.unwrap();
799 assert_eq!(cat, "Security");
800 assert_eq!(rest, "important rule");
801 }
802
803 #[test]
804 fn test_parse_no_bold() {
805 let result = AutoMemoryBridge::extract_bold_category("Just a plain item");
806 assert!(result.is_none());
807 }
808
809 #[test]
810 fn test_parse_markdown_insights() {
811 let temp_dir = tempfile::tempdir().unwrap();
812 let bridge = make_bridge(temp_dir.path());
813
814 let md = r#"# Project Patterns
815
816- **Debugging**: Use trace-level logging for async tasks
817- **Architecture**: Follow the kernel module pattern
818- **Performance**: Batch embeddings when possible
819
820## Security
821
822- Always validate input at the boundary
823- Use RBAC for multi-agent access control
824"#;
825
826 let insights = bridge.parse_markdown_insights(md);
827 assert!(!insights.is_empty());
828
829 let debugging = insights
831 .iter()
832 .find(|i| i.category == InsightCategory::Debugging);
833 assert!(debugging.is_some());
834
835 let security = insights
836 .iter()
837 .find(|i| i.category == InsightCategory::Security);
838 assert!(security.is_some());
839 }
840
841 #[test]
842 fn test_parse_empty_markdown() {
843 let temp_dir = tempfile::tempdir().unwrap();
844 let bridge = make_bridge(temp_dir.path());
845
846 let insights = bridge.parse_markdown_insights("");
847 assert!(insights.is_empty());
848 }
849
850 #[test]
851 fn test_parse_plain_text_as_single_insight() {
852 let temp_dir = tempfile::tempdir().unwrap();
853 let bridge = make_bridge(temp_dir.path());
854
855 let text = "This is a plain text memory entry without markdown formatting.";
856 let insights = bridge.parse_markdown_insights(text);
857 assert_eq!(insights.len(), 1);
858 assert_eq!(insights[0].category, InsightCategory::General);
859 }
860
861 #[test]
862 fn test_insight_category_parsing() {
863 assert_eq!(
864 InsightCategory::from_str_loose("patterns"),
865 InsightCategory::ProjectPatterns
866 );
867 assert_eq!(
868 InsightCategory::from_str_loose("debug"),
869 InsightCategory::Debugging
870 );
871 assert_eq!(
872 InsightCategory::from_str_loose("arch"),
873 InsightCategory::Architecture
874 );
875 assert_eq!(
876 InsightCategory::from_str_loose("perf"),
877 InsightCategory::Performance
878 );
879 assert_eq!(
880 InsightCategory::from_str_loose("sec"),
881 InsightCategory::Security
882 );
883 assert_eq!(
884 InsightCategory::from_str_loose("unknown"),
885 InsightCategory::General
886 );
887 }
888
889 #[test]
890 fn test_confidence_bar() {
891 let bar = format_confidence_bar(0.8);
892 assert!(bar.contains("80%"));
893
894 let bar_low = format_confidence_bar(0.2);
895 assert!(bar_low.contains("20%"));
896 }
897
898 #[tokio::test]
899 async fn test_import_from_empty_dir() {
900 let temp_dir = tempfile::tempdir().unwrap();
901 let bridge = make_bridge(temp_dir.path());
902
903 let result = bridge.import_from_auto().await.unwrap();
905 assert_eq!(result.imported, 0);
906 assert_eq!(result.failed, 0);
907 }
908
909 #[tokio::test]
910 async fn test_import_from_file() {
911 let temp_dir = tempfile::tempdir().unwrap();
912 let auto_dir = temp_dir.path().join("auto");
913 tokio::fs::create_dir_all(&auto_dir).await.unwrap();
914
915 let md = r#"- **Debugging**: Use println! for quick debugging
917- **Architecture**: Keep modules small and focused
918"#;
919 tokio::fs::write(auto_dir.join("MEMORY.md"), md)
920 .await
921 .unwrap();
922
923 let bridge = make_bridge(temp_dir.path());
924 let result = bridge.import_from_auto().await.unwrap();
925 assert!(result.imported >= 1, "Should import at least 1 insight");
926 }
927
928 #[tokio::test]
929 async fn test_export_to_auto() {
930 let temp_dir = tempfile::tempdir().unwrap();
931 let bridge = make_bridge(temp_dir.path());
932
933 let patterns = vec![
934 GuidancePattern {
935 id: "p1".to_string(),
936 category: InsightCategory::Debugging,
937 description: "Always check error chains".to_string(),
938 confidence: 0.9,
939 usage_count: 5,
940 },
941 GuidancePattern {
942 id: "p2".to_string(),
943 category: InsightCategory::Architecture,
944 description: "Use actor model for concurrency".to_string(),
945 confidence: 0.7,
946 usage_count: 3,
947 },
948 ];
949
950 let result = bridge.export_to_auto(&patterns).await.unwrap();
951 assert_eq!(result.exported, 2);
952 assert!(result.categories_updated >= 2);
953
954 assert!(bridge.auto_memory_dir.join("MEMORY.md").exists());
956 assert!(bridge.auto_memory_dir.join("debugging.md").exists());
957 assert!(bridge.auto_memory_dir.join("architecture.md").exists());
958
959 let main = tokio::fs::read_to_string(bridge.auto_memory_dir.join("MEMORY.md"))
961 .await
962 .unwrap();
963 assert!(main.contains("Oxios Memory"));
964 assert!(main.contains("Always check error chains"));
965 assert!(main.contains("Use actor model for concurrency"));
966 }
967
968 #[tokio::test]
969 async fn test_bidirectional_sync() {
970 let temp_dir = tempfile::tempdir().unwrap();
971 let auto_dir = temp_dir.path().join("auto");
972 tokio::fs::create_dir_all(&auto_dir).await.unwrap();
973
974 tokio::fs::write(
976 auto_dir.join("MEMORY.md"),
977 "- **Debugging**: Test insight for sync",
978 )
979 .await
980 .unwrap();
981
982 let bridge = make_bridge(temp_dir.path());
983 let result = bridge
984 .sync_session(SyncDirection::Bidirectional)
985 .await
986 .unwrap();
987
988 assert!(result.import.imported >= 1 || result.import.skipped_duplicates > 0);
990
991 }
993
994 #[test]
995 fn test_sync_direction_serialization() {
996 let dir = SyncDirection::ToAuto;
997 let json = serde_json::to_string(&dir).unwrap();
998 assert_eq!(json, "\"to_auto\"");
999
1000 let parsed: SyncDirection = serde_json::from_str(&json).unwrap();
1001 assert_eq!(parsed, SyncDirection::ToAuto);
1002 }
1003}