use kimun_notes::cli::commands::journal::JournalSubcommand;
use kimun_notes::cli::commands::{JournalArgs, NoteSubcommand};
use kimun_notes::cli::{CliCommand, run_cli};
use tempfile::TempDir;
async fn write_config(config_path: &std::path::Path, workspace_dir: &std::path::Path) {
let content = format!(
r#"config_version = 2
[global]
current_workspace = "default"
theme = "Nord"
[workspaces.default]
path = "{}"
last_paths = []
created = "2026-01-01T00:00:00Z"
"#,
workspace_dir.display()
);
std::fs::write(config_path, content).unwrap();
let vault = kimun_core::NoteVault::new(workspace_dir).await.unwrap();
vault.validate_and_init().await.unwrap();
}
#[tokio::test]
async fn test_note_create_creates_new_note() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Create {
path: "my-note".to_string(),
content: Some("# My Note\n\nHello".to_string()),
},
},
Some(config_path),
)
.await;
assert!(result.is_ok(), "note create should succeed: {:?}", result);
let note_file = workspace_dir.path().join("my-note.md");
assert!(
note_file.exists(),
"note file should exist at {:?}",
note_file
);
let content = std::fs::read_to_string(¬e_file).unwrap();
assert!(
content.contains("Hello"),
"note should contain the provided content"
);
}
#[tokio::test]
async fn test_note_create_fails_if_note_exists() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(workspace_dir.path().join("existing.md"), "# Existing").unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Create {
path: "existing".to_string(),
content: Some("new content".to_string()),
},
},
Some(config_path),
)
.await;
assert!(
result.is_err(),
"note create should fail when note already exists"
);
let err = format!("{:?}", result.unwrap_err());
assert!(
err.contains("already exists"),
"error should mention 'already exists': {}",
err
);
}
#[tokio::test]
async fn test_note_create_uses_quick_note_path() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
let content = format!(
r#"config_version = 2
[global]
current_workspace = "default"
theme = "Nord"
[workspaces.default]
path = "{}"
last_paths = []
created = "2026-01-01T00:00:00Z"
quick_note_path = "/inbox"
"#,
workspace_dir.path().display()
);
std::fs::write(&config_path, content).unwrap();
kimun_core::NoteVault::new(workspace_dir.path())
.await
.unwrap()
.validate_and_init()
.await
.unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Create {
path: "idea".to_string(),
content: Some("an idea".to_string()),
},
},
Some(config_path),
)
.await;
assert!(result.is_ok(), "note create should succeed: {:?}", result);
let note_file = workspace_dir.path().join("inbox").join("idea.md");
assert!(note_file.exists(), "note should be at {:?}", note_file);
}
#[tokio::test]
async fn test_note_create_absolute_path_ignores_quick_note_path() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
let content = format!(
r#"config_version = 2
[global]
current_workspace = "default"
theme = "Nord"
[workspaces.default]
path = "{}"
last_paths = []
created = "2026-01-01T00:00:00Z"
quick_note_path = "/inbox"
"#,
workspace_dir.path().display()
);
std::fs::write(&config_path, content).unwrap();
kimun_core::NoteVault::new(workspace_dir.path())
.await
.unwrap()
.validate_and_init()
.await
.unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Create {
path: "/projects/plan".to_string(),
content: Some("a plan".to_string()),
},
},
Some(config_path),
)
.await;
assert!(result.is_ok(), "note create should succeed: {:?}", result);
let note_file = workspace_dir.path().join("projects").join("plan.md");
assert!(note_file.exists(), "note should be at {:?}", note_file);
}
#[tokio::test]
async fn test_note_append_creates_if_not_exists() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Append {
path: "new-note".to_string(),
content: Some("first line".to_string()),
},
},
Some(config_path),
)
.await;
assert!(result.is_ok(), "note append should succeed: {:?}", result);
let note_file = workspace_dir.path().join("new-note.md");
assert!(note_file.exists(), "note should be created");
let content = std::fs::read_to_string(¬e_file).unwrap();
assert!(content.contains("first line"));
}
#[tokio::test]
async fn test_note_append_appends_to_existing() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(workspace_dir.path().join("log.md"), "# Log\n\nFirst entry").unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Append {
path: "log".to_string(),
content: Some("Second entry".to_string()),
},
},
Some(config_path),
)
.await;
assert!(result.is_ok(), "note append should succeed: {:?}", result);
let content = std::fs::read_to_string(workspace_dir.path().join("log.md")).unwrap();
assert!(
content.contains("First entry"),
"original content preserved"
);
assert!(content.contains("Second entry"), "new content appended");
}
#[tokio::test]
async fn test_note_append_empty_content_is_noop() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(workspace_dir.path().join("original.md"), "# Original").unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Append {
path: "original".to_string(),
content: Some("".to_string()),
},
},
Some(config_path),
)
.await;
assert!(result.is_ok());
let content = std::fs::read_to_string(workspace_dir.path().join("original.md")).unwrap();
assert_eq!(
content, "# Original",
"content should be unchanged on empty append"
);
}
#[tokio::test]
async fn test_journal_creates_todays_entry() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let result = run_cli(
CliCommand::Journal(JournalArgs {
date: None,
content: Some("Today's thought".to_string()),
subcommand: None,
}),
Some(config_path),
)
.await;
assert!(result.is_ok(), "journal should succeed: {:?}", result);
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
let journal_file = workspace_dir
.path()
.join("journal")
.join(format!("{}.md", today));
assert!(
journal_file.exists(),
"journal entry should exist at {:?}",
journal_file
);
let content = std::fs::read_to_string(&journal_file).unwrap();
assert!(content.contains("Today's thought"));
}
#[tokio::test]
async fn test_journal_appends_to_existing_entry() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
let journal_dir = workspace_dir.path().join("journal");
std::fs::create_dir_all(&journal_dir).unwrap();
std::fs::write(
journal_dir.join(format!("{}.md", today)),
format!("# {}\n\nFirst entry", today),
)
.unwrap();
run_cli(
CliCommand::Journal(JournalArgs {
date: None,
content: Some("Second entry".to_string()),
subcommand: None,
}),
Some(config_path),
)
.await
.unwrap();
let content = std::fs::read_to_string(journal_dir.join(format!("{}.md", today))).unwrap();
assert!(
content.contains("First entry"),
"original content preserved"
);
assert!(content.contains("Second entry"), "new content appended");
}
#[tokio::test]
async fn test_journal_empty_content_is_noop() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
let journal_dir = workspace_dir.path().join("journal");
std::fs::create_dir_all(&journal_dir).unwrap();
let journal_file = journal_dir.join(format!("{}.md", today));
std::fs::write(&journal_file, format!("# {}", today)).unwrap();
run_cli(
CliCommand::Journal(JournalArgs {
date: None,
content: Some("".to_string()),
subcommand: None,
}),
Some(config_path),
)
.await
.unwrap();
let content = std::fs::read_to_string(&journal_file).unwrap();
assert_eq!(
content,
format!("# {}", today),
"content should be unchanged on empty journal"
);
}
#[tokio::test]
async fn test_journal_date_creates_specific_entry() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
run_cli(
CliCommand::Journal(JournalArgs {
date: Some("2024-01-15".to_string()),
content: Some("Backdated entry".to_string()),
subcommand: None,
}),
Some(config_path),
)
.await
.unwrap();
let journal_file = workspace_dir.path().join("journal").join("2024-01-15.md");
assert!(
journal_file.exists(),
"specific-date journal entry should exist"
);
let content = std::fs::read_to_string(&journal_file).unwrap();
assert!(content.contains("Backdated entry"));
}
#[tokio::test]
async fn test_journal_invalid_date_returns_error() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let result = run_cli(
CliCommand::Journal(JournalArgs {
date: Some("not-a-date".to_string()),
content: Some("content".to_string()),
subcommand: None,
}),
Some(config_path),
)
.await;
assert!(result.is_err(), "invalid date should return an error");
let msg = format!("{:?}", result.unwrap_err());
assert!(
msg.contains("Invalid date"),
"error should mention invalid date, got: {}",
msg
);
}
#[tokio::test]
async fn test_journal_show_missing_entry_returns_error() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
let result = run_cli(
CliCommand::Journal(JournalArgs {
date: None,
content: None,
subcommand: Some(JournalSubcommand::Show {
date: Some("1999-01-01".to_string()),
format: kimun_notes::cli::output::OutputFormat::Text,
}),
}),
Some(config_path),
)
.await;
assert!(
result.is_err(),
"show on missing entry should return an error"
);
let msg = format!("{:?}", result.unwrap_err());
assert!(msg.contains("No journal entry found"), "got: {}", msg);
}
#[tokio::test]
async fn test_note_show_text_returns_ok() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(
workspace_dir.path().join("my-note.md"),
"# My Note\n\nHello world",
)
.unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Show {
paths: vec!["my-note".to_string()],
format: kimun_notes::cli::output::OutputFormat::Text,
},
},
Some(config_path),
)
.await;
assert!(result.is_ok(), "note show should succeed: {:?}", result);
}
#[tokio::test]
async fn test_note_show_missing_note_fails() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(workspace_dir.path().join("unrelated.md"), "# Unrelated").unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Show {
paths: vec!["does-not-exist".to_string()],
format: kimun_notes::cli::output::OutputFormat::Text,
},
},
Some(config_path),
)
.await;
assert!(result.is_err(), "note show on missing note should fail");
}
#[tokio::test]
async fn test_note_show_json_returns_ok() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(
workspace_dir.path().join("json-note.md"),
"# JSON Note\n\nsome content",
)
.unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Show {
paths: vec!["json-note".to_string()],
format: kimun_notes::cli::output::OutputFormat::Json,
},
},
Some(config_path),
)
.await;
assert!(
result.is_ok(),
"note show --format json should succeed: {:?}",
result
);
}
#[tokio::test]
async fn test_note_show_multiple_notes_ok() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(workspace_dir.path().join("note-a.md"), "# Note A").unwrap();
std::fs::write(workspace_dir.path().join("note-b.md"), "# Note B").unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Show {
paths: vec!["note-a".to_string(), "note-b".to_string()],
format: kimun_notes::cli::output::OutputFormat::Text,
},
},
Some(config_path),
)
.await;
assert!(
result.is_ok(),
"note show with multiple notes should succeed: {:?}",
result
);
}
#[tokio::test]
async fn test_note_show_format_paths_returns_error() {
use kimun_core::nfs::VaultPath;
use kimun_notes::cli::output::OutputFormat;
let dir = TempDir::new().unwrap();
let vault = kimun_core::NoteVault::new(dir.path()).await.unwrap();
vault.validate_and_init().await.unwrap();
vault
.create_note(
&VaultPath::note_path_from("test/note"),
"# Test\n\nContent.",
)
.await
.unwrap();
let config_path = dir.path().join("config.toml");
write_config(&config_path, dir.path()).await;
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Show {
paths: vec!["test/note".to_string()],
format: OutputFormat::Paths,
},
},
Some(config_path),
)
.await;
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("--format paths is not valid for note show"),
"got: {}",
msg
);
}
#[tokio::test]
async fn test_note_show_partial_failure_returns_err() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
write_config(&config_path, workspace_dir.path()).await;
std::fs::write(workspace_dir.path().join("exists.md"), "# Exists").unwrap();
let result = run_cli(
CliCommand::Note {
subcommand: NoteSubcommand::Show {
paths: vec!["exists".to_string(), "missing".to_string()],
format: kimun_notes::cli::output::OutputFormat::Text,
},
},
Some(config_path),
)
.await;
assert!(result.is_err(), "partial failure should return Err");
}