use kimun_core::nfs::VaultPath;
use kimun_core::{NoteVault, VaultConfig};
use kimun_notes::cli::commands::workspace::WorkspaceSubcommand;
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_workspace(name: &str, dir: &TempDir) -> NoteVault {
let vault = NoteVault::new(VaultConfig::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(format!("{}-project", name)),
&format!(
"# {} Project\n\n#programming #{}\n\nThis is the {} project note.",
name, name, name
),
)
.await
.expect("failed to create project note");
vault
.create_note(
&VaultPath::note_path_from(format!("journal/{}-daily", name)),
&format!(
"# {} Daily Journal\n\n## Today\n\nDaily activities for {}.",
name, name
),
)
.await
.expect("failed to create journal note");
vault
.create_note(
&VaultPath::note_path_from(format!("notes/{}-research", name)),
&format!(
"# {} Research\n\n[[{}-project]]\n\nResearch notes for {}.",
name, name, name
),
)
.await
.expect("failed to create research note");
vault
.recreate_index()
.await
.expect("failed to recreate index");
vault
}
fn write_phase1_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_multi_workspace_init_and_switch() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace1_dir = TempDir::new().unwrap();
let workspace2_dir = TempDir::new().unwrap();
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("work".to_string()),
path: workspace1_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("personal".to_string()),
path: workspace2_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"second workspace init should succeed: {:?}",
result
);
let settings = AppSettings::load_from_file(config_path.clone()).expect("should load settings");
let ws_config = settings
.workspace_config
.as_ref()
.expect("should have workspace config");
assert_eq!(ws_config.workspaces.len(), 2, "should have 2 workspaces");
assert!(
ws_config.workspaces.contains_key("work"),
"should have work workspace"
);
assert!(
ws_config.workspaces.contains_key("personal"),
"should have personal workspace"
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Use {
name: "personal".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace switch should succeed: {:?}",
result
);
let settings = AppSettings::load_from_file(config_path.clone()).expect("should load settings");
let ws_config = settings
.workspace_config
.as_ref()
.expect("should have workspace config");
assert_eq!(
ws_config.global.current_workspace, "personal",
"should switch to personal workspace"
);
}
#[tokio::test]
async fn test_workspace_isolation() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let work_dir = TempDir::new().unwrap();
let personal_dir = TempDir::new().unwrap();
setup_test_workspace("work", &work_dir).await;
setup_test_workspace("personal", &personal_dir).await;
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("work".to_string()),
path: work_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"work workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("personal".to_string()),
path: personal_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"personal workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Search {
query: "work".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search in work workspace should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Use {
name: "personal".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace switch should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Search {
query: "personal".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search in personal workspace should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Search {
query: "work-project".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search for work content in personal should succeed (but find nothing): {:?}",
result
);
}
#[tokio::test]
async fn test_json_output_multi_workspace() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
setup_test_workspace("test", &workspace_dir).await;
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("test-workspace".to_string()),
path: workspace_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"test workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Search {
query: "test".to_string(),
format: OutputFormat::Json,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search with JSON format should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Notes {
path: None,
format: OutputFormat::Json,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"notes with JSON format should succeed: {:?}",
result
);
let vault = NoteVault::new(VaultConfig::new(workspace_dir.path()))
.await
.unwrap();
vault.validate_and_init().await.unwrap();
let results = vault.search_notes("test").await.unwrap();
let json_str = kimun_notes::cli::json_output::format_notes_as_json(
&vault,
&results,
"test-workspace",
Some("test"),
false, )
.await
.expect("format_notes_as_json should succeed");
let json: serde_json::Value =
serde_json::from_str(&json_str).expect("output should be valid JSON");
assert_eq!(json["metadata"]["workspace"], "test-workspace");
assert_eq!(
json["metadata"]["workspace_path"],
workspace_dir.path().to_string_lossy().to_string()
);
assert_eq!(json["metadata"]["query"], "test");
assert!(!json["metadata"]["is_listing"].as_bool().unwrap());
let notes = json["notes"].as_array().expect("should have notes array");
assert!(!notes.is_empty(), "should find test notes");
for note in notes {
assert!(
note["metadata"].is_object(),
"note should have metadata object"
);
assert!(
note["metadata"]["tags"].is_array(),
"metadata should have tags array"
);
assert!(
note["metadata"]["links"].is_array(),
"metadata should have links array"
);
assert!(
note["metadata"]["headers"].is_array(),
"metadata should have headers array"
);
}
}
#[tokio::test]
async fn test_phase1_to_phase2_migration() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
setup_test_workspace("legacy", &workspace_dir).await;
write_phase1_config(&config_path, workspace_dir.path());
let settings =
AppSettings::load_from_file(config_path.clone()).expect("should load Phase 1 config");
assert!(
settings.workspace_dir.is_none(),
"workspace_dir should be None after migration"
);
assert!(
settings.workspace_config.is_some(),
"should have migrated workspace_config"
);
let ws_config = settings.workspace_config.as_ref().unwrap();
assert_eq!(
ws_config.global.current_workspace, "default",
"should migrate to default workspace"
);
assert_eq!(
ws_config.workspaces.len(),
1,
"should have one workspace after migration"
);
let default_workspace = ws_config
.workspaces
.get("default")
.expect("should have default workspace");
assert_eq!(
default_workspace.path,
workspace_dir.path(),
"migrated workspace should have correct path"
);
let result = run_cli(
CliCommand::Search {
query: "legacy".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search should work with migrated config: {:?}",
result
);
let result = run_cli(
CliCommand::Notes {
path: None,
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"notes should work with migrated config: {:?}",
result
);
}
#[tokio::test]
async fn test_workspace_management_commands() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace1_dir = TempDir::new().unwrap();
let workspace2_dir = TempDir::new().unwrap();
let workspace3_dir = TempDir::new().unwrap();
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("alpha".to_string()),
path: workspace1_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"alpha workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("beta".to_string()),
path: workspace2_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"beta workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::List,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace list should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Rename {
old_name: "beta".to_string(),
new_name: "gamma".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace rename should succeed: {:?}",
result
);
let settings = AppSettings::load_from_file(config_path.clone()).expect("should load settings");
let ws_config = settings
.workspace_config
.as_ref()
.expect("should have workspace config");
assert!(
ws_config.workspaces.contains_key("gamma"),
"should have gamma workspace"
);
assert!(
!ws_config.workspaces.contains_key("beta"),
"should not have beta workspace"
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("temp".to_string()),
path: workspace3_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"temp workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Remove {
name: "temp".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace remove should succeed: {:?}",
result
);
let settings = AppSettings::load_from_file(config_path.clone()).expect("should load settings");
let ws_config = settings
.workspace_config
.as_ref()
.expect("should have workspace config");
assert!(
!ws_config.workspaces.contains_key("temp"),
"should not have temp workspace"
);
assert_eq!(
ws_config.workspaces.len(),
2,
"should have 2 workspaces remaining"
);
}
#[tokio::test]
async fn test_workspace_reindex() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("reindex-test".to_string()),
path: workspace_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace init should succeed: {:?}",
result
);
setup_test_workspace("reindex", &workspace_dir).await;
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Reindex {
name: Some("reindex-test".to_string()),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace reindex should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Search {
query: "reindex".to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search after reindex should succeed: {:?}",
result
);
}
#[tokio::test]
async fn test_error_handling() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let workspace_dir = TempDir::new().unwrap();
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some("existing".to_string()),
path: workspace_dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace init should succeed: {:?}",
result
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Use {
name: "non-existent".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_err(),
"switching to non-existent workspace should fail"
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Remove {
name: "non-existent".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_err(),
"removing non-existent workspace should fail"
);
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Rename {
old_name: "non-existent".to_string(),
new_name: "new-name".to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_err(),
"renaming non-existent workspace should fail"
);
}
#[tokio::test]
async fn test_complex_multi_workspace_workflow() {
let config_dir = TempDir::new().unwrap();
let config_path = config_dir.path().join("config.toml");
let work_dir = TempDir::new().unwrap();
let personal_dir = TempDir::new().unwrap();
let project_dir = TempDir::new().unwrap();
setup_test_workspace("work", &work_dir).await;
setup_test_workspace("personal", &personal_dir).await;
setup_test_workspace("project", &project_dir).await;
for (name, dir) in [
("work", &work_dir),
("personal", &personal_dir),
("project", &project_dir),
] {
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Init {
name: Some(name.to_string()),
path: dir.path().to_path_buf(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"{} workspace init should succeed: {:?}",
name,
result
);
}
for workspace_name in ["work", "personal", "project"] {
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::Use {
name: workspace_name.to_string(),
},
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"switch to {} should succeed: {:?}",
workspace_name,
result
);
let result = run_cli(
CliCommand::Search {
query: workspace_name.to_string(),
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"search in {} should succeed: {:?}",
workspace_name,
result
);
let result = run_cli(
CliCommand::Notes {
path: None,
format: OutputFormat::Text,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"notes in {} should succeed: {:?}",
workspace_name,
result
);
let result = run_cli(
CliCommand::Search {
query: workspace_name.to_string(),
format: OutputFormat::Json,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"JSON search in {} should succeed: {:?}",
workspace_name,
result
);
}
let result = run_cli(
CliCommand::Workspace {
subcommand: WorkspaceSubcommand::List,
},
Some(config_path.clone()),
)
.await;
assert!(
result.is_ok(),
"workspace list should succeed: {:?}",
result
);
let settings = AppSettings::load_from_file(config_path.clone()).expect("should load settings");
let ws_config = settings
.workspace_config
.as_ref()
.expect("should have workspace config");
assert_eq!(ws_config.workspaces.len(), 3, "should have 3 workspaces");
assert_eq!(
ws_config.global.current_workspace, "project",
"should be on project workspace"
);
}