use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn test_dir() -> PathBuf {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir =
std::env::temp_dir().join(format!("mneme_obsidian_test_{}_{}", std::process::id(), id));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn test_db_path() -> PathBuf {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
std::env::temp_dir().join(format!(
"mneme_obsidian_db_{}_{}.db",
std::process::id(),
id
))
}
use mneme::export::obsidian;
use mneme::store::db::Database;
use mneme::store::memory::{CreateMemoryInput, Importance, Memory, MemoryType, Scope};
fn make_db() -> Database {
Database::open(&test_db_path()).unwrap()
}
fn make_memory(project: &str, title: &str, content: &str) -> Memory {
let input = CreateMemoryInput {
encrypt: false,
project: project.to_string(),
scope: Some(Scope::Project),
title: title.to_string(),
content: content.to_string(),
what: Some(format!("What: {}", title)),
why: Some(format!("Why: {}", title)),
context: None,
learned: Some(format!("Learned: {}", title)),
memory_type: MemoryType::from_str("decision").unwrap(),
importance: Importance::High,
tags: vec!["test".to_string(), "obsidian".to_string()],
topic_key: Some(format!("test/{}", title.to_lowercase())),
capture_prompt: None,
valid_from: None,
valid_until: None,
provenance: None,
};
make_db().memories().save(input, None, None).unwrap()
}
#[test]
fn test_obsidian_export_creates_vault() {
let dir = test_dir();
let m1 = make_memory("proj1", "Memory One", "Content 1");
let m2 = make_memory("proj1", "Memory Two", "Content 2");
let stats = obsidian::export_to_obsidian(&[m1, m2], &[], "proj1", &dir).unwrap();
assert!(stats.files_written >= 2);
let vault = dir.join("mneme-export");
assert!(vault.exists());
assert!(vault.join("memories").exists());
assert!(vault.join("README.md").exists());
}
#[test]
fn test_obsidian_filename_sanitization() {
let dir = test_dir();
let mem = make_memory("p", "Test/File:Name?", "c");
obsidian::export_to_obsidian(&[mem], &[], "p", &dir).unwrap();
let files: Vec<_> = std::fs::read_dir(dir.join("mneme-export/memories"))
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert_eq!(files.len(), 1);
let fname = files[0].file_name().to_string_lossy().to_string();
assert!(!fname.contains('/'));
assert!(!fname.contains(':'));
assert!(!fname.contains('?'));
}
#[test]
fn test_obsidian_export_includes_frontmatter() {
let dir = test_dir();
let mem = make_memory("p", "Frontmatter Test", "Content");
obsidian::export_to_obsidian(&[mem], &[], "p", &dir).unwrap();
let file_path = dir.join("mneme-export/memories/Frontmatter Test.md");
assert!(file_path.exists());
let content = std::fs::read_to_string(&file_path).unwrap();
assert!(content.starts_with("---\n"));
assert!(content.contains("title: \"Frontmatter Test\""));
assert!(content.contains("type: decision"));
assert!(content.contains("importance: high"));
}
#[test]
fn test_obsidian_export_includes_obsidian_tags() {
let dir = test_dir();
let mem = make_memory("p", "Tagged Memory", "Content");
obsidian::export_to_obsidian(&[mem], &[], "p", &dir).unwrap();
let file_path = dir.join("mneme-export/memories/Tagged Memory.md");
let content = std::fs::read_to_string(&file_path).unwrap();
assert!(content.contains("#test") || content.contains("#obsidian"));
}
#[test]
fn test_obsidian_export_index_readme() {
let dir = test_dir();
let m1 = make_memory("p", "Alpha", "Content");
let m2 = make_memory("p", "Beta", "Content");
obsidian::export_to_obsidian(&[m1, m2], &[], "p", &dir).unwrap();
let readme = dir.join("mneme-export/README.md");
assert!(readme.exists());
let content = std::fs::read_to_string(&readme).unwrap();
assert!(content.contains("Alpha") || content.contains("Beta"));
assert!(content.contains("p"));
}
#[test]
fn test_obsidian_export_empty_memories() {
let dir = test_dir();
let stats = obsidian::export_to_obsidian(&[], &[], "p", &dir).unwrap();
assert!(stats.files_written >= 1);
}
#[test]
fn test_obsidian_export_graph_json() {
let dir = test_dir();
let mem = make_memory("p", "Test", "Content");
obsidian::export_to_obsidian(&[mem], &[], "p", &dir).unwrap();
let graph_path = dir.join("mneme-export/.graph/graph.json");
assert!(graph_path.exists());
let content = std::fs::read_to_string(&graph_path).unwrap();
let graph: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(graph["nodes"].is_array());
assert!(graph["edges"].is_array());
}
#[test]
fn test_obsidian_export_obsidian_metadata() {
let dir = test_dir();
obsidian::export_to_obsidian(&[], &[], "p", &dir).unwrap();
let app_json = dir.join("mneme-export/.obsidian/app.json");
assert!(app_json.exists());
let content = std::fs::read_to_string(&app_json).unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(json["baseApp"], "obsidian");
}
#[test]
fn test_obsidian_wikilinks_between_related() {
use mneme::store::memory::{CreateRelationInput, RelationType};
use uuid::Uuid;
let dir = test_dir();
let m1 = make_memory("p", "Source Memory", "Content 1");
let m2 = make_memory("p", "Target Memory", "Content 2");
let relation = CreateRelationInput {
source_id: m1.id,
target_id: m2.id,
relation_type: RelationType::Extends,
confidence: Some(0.9),
reason: Some("Test relation".to_string()),
};
let relation = mneme::store::memory::MemoryRelation {
id: Uuid::new_v4(),
sync_id: Uuid::new_v4().to_string(),
source_id: m1.id,
target_id: m2.id,
relation_type: RelationType::Extends,
confidence: 0.9,
judgment_status: "active".to_string(),
reason: Some("Test".to_string()),
evidence: None,
marked_by_actor: "test".to_string(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let _ = relation;
let _ = relation;
obsidian::export_to_obsidian(&[m1.clone(), m2], &[relation], "p", &dir).unwrap();
let source_path = dir.join("mneme-export/memories/Source Memory.md");
let content = std::fs::read_to_string(&source_path).unwrap();
assert!(source_path.exists());
}