use kimun_core::NoteVault;
use kimun_core::nfs::VaultPath;
use kimun_notes::cli::output::OutputFormat;
use kimun_notes::cli::{CliCommand, run_cli};
use kimun_notes::settings::AppSettings;
use tempfile::TempDir;
async fn setup_test_vault(dir: &TempDir) -> NoteVault {
let vault = NoteVault::new(dir.path())
.await
.expect("failed to create vault");
vault
.validate_and_init()
.await
.expect("failed to init vault");
vault
.create_note(
&VaultPath::note_path_from("hello"),
"# Hello World\n\nThis is a hello note.",
)
.await
.expect("failed to create hello note");
vault
.create_note(
&VaultPath::note_path_from("sub/nested"),
"# Nested Note\n\nThis note lives in a subdirectory.",
)
.await
.expect("failed to create nested note");
vault
.recreate_index()
.await
.expect("failed to recreate index");
vault
}
fn write_config(config_path: &std::path::Path, workspace: &std::path::Path) {
let toml = format!(
"workspace_dir = {:?}\n",
workspace.to_string_lossy().as_ref()
);
std::fs::write(config_path, toml).expect("failed to write config file");
}
#[tokio::test]
async fn test_cli_search_command() {
let workspace_dir = TempDir::new().unwrap();
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
setup_test_vault(&workspace_dir).await;
write_config(&config_path, workspace_dir.path());
let result = run_cli(
CliCommand::Search {
query: "hello".to_string(),
format: OutputFormat::Text,
},
Some(config_path),
)
.await;
assert!(
result.is_ok(),
"search command should succeed: {:?}",
result
);
}
#[tokio::test]
async fn test_cli_notes_command() {
let workspace_dir = TempDir::new().unwrap();
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
setup_test_vault(&workspace_dir).await;
write_config(&config_path, workspace_dir.path());
let result = run_cli(
CliCommand::Notes {
path: None,
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"notes command (no filter) should succeed: {:?}",
result
);
let result_filtered = run_cli(
CliCommand::Notes {
path: Some("sub/".to_string()),
format: OutputFormat::Text,
},
Some(config_path),
)
.await;
assert!(
result_filtered.is_ok(),
"notes command (with path filter) should succeed: {:?}",
result_filtered
);
}
#[tokio::test]
async fn test_cli_no_workspace_error() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
std::fs::write(&config_path, "# empty config\n").unwrap();
let settings = AppSettings::load_from_file(config_path).expect("settings should load");
assert!(
settings.workspace_dir.is_none(),
"workspace_dir should be None when not set in config"
);
}
#[tokio::test]
async fn test_cli_custom_config() {
let workspace_dir = TempDir::new().unwrap();
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("custom_config.toml");
setup_test_vault(&workspace_dir).await;
write_config(&config_path, workspace_dir.path());
let settings = AppSettings::load_from_file(config_path.clone()).expect("settings should load");
let ws_config = settings
.workspace_config
.as_ref()
.expect("--config flag should load settings from the specified file");
let default_ws = ws_config
.workspaces
.get("default")
.expect("should have 'default' workspace after migration");
assert_eq!(
default_ws.path.as_path(),
workspace_dir.path(),
"--config flag should load settings from the specified file"
);
let result = run_cli(
CliCommand::Notes {
path: None,
format: OutputFormat::Text,
},
Some(config_path),
)
.await;
assert!(
result.is_ok(),
"notes command with custom config should succeed: {:?}",
result
);
}
async fn setup_exclusion_test_vault(dir: &TempDir) -> NoteVault {
let vault = NoteVault::new(dir.path())
.await
.expect("failed to create vault");
vault
.validate_and_init()
.await
.expect("failed to init vault");
vault
.create_note(
&VaultPath::note_path_from("weekly-meeting"),
"# Weekly Meeting\n\nRegular team meeting notes.",
)
.await
.expect("failed to create meeting note");
vault
.create_note(
&VaultPath::note_path_from("cancelled-meeting"),
"# Cancelled Meeting\n\nThis meeting was cancelled.",
)
.await
.expect("failed to create cancelled note");
vault
.create_note(
&VaultPath::note_path_from("project-draft"),
"# Project Draft\n\nDraft version of project proposal.",
)
.await
.expect("failed to create draft note");
vault
.create_note(
&VaultPath::note_path_from("project-final"),
"# Project Final\n\nFinal version of project proposal.",
)
.await
.expect("failed to create final note");
vault
.recreate_index()
.await
.expect("failed to recreate index");
vault
}
#[tokio::test]
async fn test_cli_search_basic_exclusions() {
let workspace_dir = TempDir::new().unwrap();
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let vault = setup_exclusion_test_vault(&workspace_dir).await;
write_config(&config_path, workspace_dir.path());
let result = run_cli(
CliCommand::Search {
query: "meeting -cancelled".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search with exclusion should succeed: {:?}",
result
);
let search_results = vault
.search_notes("meeting -cancelled")
.await
.expect("direct search should work");
let paths: Vec<String> = search_results
.iter()
.map(|(entry, _)| entry.path.to_string())
.collect();
assert!(
paths.contains(&"/weekly-meeting.md".to_string()),
"Should find weekly-meeting note; found: {:?}",
paths
);
assert!(
!paths.contains(&"/cancelled-meeting.md".to_string()),
"Should exclude cancelled-meeting note; found: {:?}",
paths
);
}
#[tokio::test]
async fn test_cli_search_compound_exclusions() {
let workspace_dir = TempDir::new().unwrap();
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let vault = setup_exclusion_test_vault(&workspace_dir).await;
write_config(&config_path, workspace_dir.path());
let result = run_cli(
CliCommand::Search {
query: "@project @-draft".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"filename exclusion should succeed: {:?}",
result
);
let search_results = vault
.search_notes("@project @-draft")
.await
.expect("direct search should work");
let paths: Vec<String> = search_results
.iter()
.map(|(entry, _)| entry.path.to_string())
.collect();
assert!(
paths.contains(&"/project-final.md".to_string()),
"Should find project-final note; found: {:?}",
paths
);
assert!(
!paths.contains(&"/project-draft.md".to_string()),
"Should exclude project-draft note; found: {:?}",
paths
);
let result = run_cli(
CliCommand::Search {
query: "@final @-draft".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"filename exclusion with final should succeed: {:?}",
result
);
let search_results = vault
.search_notes("@final @-draft")
.await
.expect("direct search should work");
let paths: Vec<String> = search_results
.iter()
.map(|(entry, _)| entry.path.to_string())
.collect();
assert!(
paths.contains(&"/project-final.md".to_string()),
"Should find project-final note; found: {:?}",
paths
);
assert!(
!paths.contains(&"/project-draft.md".to_string()),
"Should exclude project-draft note; found: {:?}",
paths
);
}
#[tokio::test]
async fn test_search_paths_format_returns_bare_paths() {
let dir = TempDir::new().unwrap();
let _vault = setup_test_vault(&dir).await;
let config_path = dir.path().join("config.toml");
write_config(&config_path, dir.path());
let result = run_cli(
CliCommand::Search {
query: "hello".to_string(),
format: OutputFormat::Paths,
},
Some(config_path),
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_cli_search_exclusion_only() {
let workspace_dir = TempDir::new().unwrap();
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let vault = setup_exclusion_test_vault(&workspace_dir).await;
write_config(&config_path, workspace_dir.path());
let result = run_cli(
CliCommand::Search {
query: "-cancelled".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"exclusion-only search should succeed: {:?}",
result
);
let search_results = vault
.search_notes("-cancelled")
.await
.expect("direct search should work");
let paths: Vec<String> = search_results
.iter()
.map(|(entry, _)| entry.path.to_string())
.collect();
assert!(
!paths.contains(&"/cancelled-meeting.md".to_string()),
"Should exclude cancelled-meeting note; found: {:?}",
paths
);
assert!(
!paths.is_empty(),
"Should find other notes when excluding cancelled; found: {:?}",
paths
);
let result = run_cli(
CliCommand::Search {
query: ">-draft".to_string(),
format: OutputFormat::Text,
},
Some(config_path),
)
.await;
assert!(
result.is_ok(),
"title exclusion-only should succeed: {:?}",
result
);
let search_results = vault
.search_notes(">-draft")
.await
.expect("direct search should work");
let paths: Vec<String> = search_results
.iter()
.map(|(entry, _)| entry.path.to_string())
.collect();
assert!(
!paths.contains(&"/project-draft.md".to_string()),
"Should exclude project-draft note; found: {:?}",
paths
);
assert!(
!paths.is_empty(),
"Should find other notes when excluding draft title; found: {:?}",
paths
);
}
#[tokio::test]
async fn test_notes_paths_format_returns_bare_paths() {
let dir = TempDir::new().unwrap();
let _vault = setup_test_vault(&dir).await;
let config_path = dir.path().join("config.toml");
write_config(&config_path, dir.path());
let result = run_cli(
CliCommand::Notes {
path: None,
format: OutputFormat::Paths,
},
Some(config_path),
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_paths_format_empty_results() {
let dir = TempDir::new().unwrap();
let _vault = setup_test_vault(&dir).await;
let config_path = dir.path().join("config.toml");
write_config(&config_path, dir.path());
let result = run_cli(
CliCommand::Notes {
path: Some("nonexistent/prefix".to_string()),
format: OutputFormat::Paths,
},
Some(config_path),
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_paths_format_path_with_spaces() {
let dir = TempDir::new().unwrap();
let vault = NoteVault::new(dir.path()).await.unwrap();
vault.validate_and_init().await.unwrap();
vault
.create_note(
&VaultPath::note_path_from("notes/first note"),
"# First\n\nContent.",
)
.await
.unwrap();
vault
.create_note(
&VaultPath::note_path_from("notes/second note"),
"# Second\n\nContent.",
)
.await
.unwrap();
vault.recreate_index().await.unwrap();
let config_path = dir.path().join("config.toml");
write_config(&config_path, dir.path());
let result = run_cli(
CliCommand::Notes {
path: Some("notes/".to_string()),
format: OutputFormat::Paths,
},
Some(config_path),
)
.await;
assert!(result.is_ok());
}