diff --git a/src-tauri/src/commands/git.rs b/src-tauri/src/commands/git.rs index 1b2737c8..e2481e42 100644 --- a/src-tauri/src/commands/git.rs +++ b/src-tauri/src/commands/git.rs @@ -2,7 +2,6 @@ use crate::git::{ GitCommit, GitPullResult, GitPushResult, GitRemoteStatus, LastCommitInfo, ModifiedFile, PulseCommit, }; -use crate::github::{DeviceFlowPollResult, DeviceFlowStart, GitHubUser, GithubRepo}; use super::expand_tilde; @@ -231,88 +230,3 @@ pub async fn git_remote_status(_vault_path: String) -> Result Result, String> { - crate::github::github_list_repos(&token).await -} - -#[cfg(desktop)] -#[tauri::command] -pub async fn github_create_repo( - token: String, - name: String, - private: bool, -) -> Result { - crate::github::github_create_repo(&token, &name, private).await -} - -#[cfg(desktop)] -#[tauri::command] -pub fn clone_repo(url: String, token: String, local_path: String) -> Result { - let local_path = expand_tilde(&local_path); - crate::github::clone_repo(&url, &token, &local_path) -} - -#[cfg(desktop)] -#[tauri::command] -pub async fn github_device_flow_start() -> Result { - crate::github::github_device_flow_start().await -} - -#[cfg(desktop)] -#[tauri::command] -pub async fn github_device_flow_poll(device_code: String) -> Result { - crate::github::github_device_flow_poll(&device_code).await -} - -#[cfg(desktop)] -#[tauri::command] -pub async fn github_get_user(token: String) -> Result { - crate::github::github_get_user(&token).await -} - -// ── GitHub commands (mobile stubs) ────────────────────────────────────────── - -#[cfg(mobile)] -#[tauri::command] -pub async fn github_list_repos(_token: String) -> Result, String> { - Err("GitHub integration is not available on mobile".into()) -} - -#[cfg(mobile)] -#[tauri::command] -pub async fn github_create_repo( - _token: String, - _name: String, - _private: bool, -) -> Result { - Err("GitHub integration is not available on mobile".into()) -} - -#[cfg(mobile)] -#[tauri::command] -pub fn clone_repo(_url: String, _token: String, _local_path: String) -> Result { - Err("Git clone is not available on mobile".into()) -} - -#[cfg(mobile)] -#[tauri::command] -pub async fn github_device_flow_start() -> Result { - Err("GitHub integration is not available on mobile".into()) -} - -#[cfg(mobile)] -#[tauri::command] -pub async fn github_device_flow_poll(_device_code: String) -> Result { - Err("GitHub integration is not available on mobile".into()) -} - -#[cfg(mobile)] -#[tauri::command] -pub async fn github_get_user(_token: String) -> Result { - Err("GitHub integration is not available on mobile".into()) -} diff --git a/src-tauri/src/commands/github.rs b/src-tauri/src/commands/github.rs new file mode 100644 index 00000000..3f09a6f7 --- /dev/null +++ b/src-tauri/src/commands/github.rs @@ -0,0 +1,88 @@ +use crate::github::{DeviceFlowPollResult, DeviceFlowStart, GitHubUser, GithubRepo}; + +use super::expand_tilde; + +// ── GitHub commands (desktop) ─────────────────────────────────────────────── + +#[cfg(desktop)] +#[tauri::command] +pub async fn github_list_repos(token: String) -> Result, String> { + crate::github::github_list_repos(&token).await +} + +#[cfg(desktop)] +#[tauri::command] +pub async fn github_create_repo( + token: String, + name: String, + private: bool, +) -> Result { + crate::github::github_create_repo(&token, &name, private).await +} + +#[cfg(desktop)] +#[tauri::command] +pub fn clone_repo(url: String, token: String, local_path: String) -> Result { + let local_path = expand_tilde(&local_path); + crate::github::clone_repo(&url, &token, &local_path) +} + +#[cfg(desktop)] +#[tauri::command] +pub async fn github_device_flow_start() -> Result { + crate::github::github_device_flow_start().await +} + +#[cfg(desktop)] +#[tauri::command] +pub async fn github_device_flow_poll(device_code: String) -> Result { + crate::github::github_device_flow_poll(&device_code).await +} + +#[cfg(desktop)] +#[tauri::command] +pub async fn github_get_user(token: String) -> Result { + crate::github::github_get_user(&token).await +} + +// ── GitHub commands (mobile stubs) ────────────────────────────────────────── + +#[cfg(mobile)] +#[tauri::command] +pub async fn github_list_repos(_token: String) -> Result, String> { + Err("GitHub integration is not available on mobile".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub async fn github_create_repo( + _token: String, + _name: String, + _private: bool, +) -> Result { + Err("GitHub integration is not available on mobile".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub fn clone_repo(_url: String, _token: String, _local_path: String) -> Result { + Err("Git clone is not available on mobile".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub async fn github_device_flow_start() -> Result { + Err("GitHub integration is not available on mobile".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub async fn github_device_flow_poll(_device_code: String) -> Result { + Err("GitHub integration is not available on mobile".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub async fn github_get_user(_token: String) -> Result { + Err("GitHub integration is not available on mobile".into()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index f158c3f3..227405b2 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,5 +1,6 @@ mod ai; mod git; +mod github; mod system; mod vault; @@ -7,6 +8,7 @@ use std::borrow::Cow; pub use ai::*; pub use git::*; +pub use github::*; pub use system::*; pub use vault::*; @@ -88,167 +90,4 @@ mod tests { assert_eq!(parse_build_label("invalid"), "b?"); assert_eq!(parse_build_label(""), "b?"); } - - #[test] - fn test_batch_archive_notes() { - let dir = tempfile::TempDir::new().unwrap(); - let note = dir.path().join("note.md"); - std::fs::write(¬e, "---\nStatus: Active\n---\n# Note\n").unwrap(); - - let result = batch_archive_notes(vec![note.to_str().unwrap().to_string()]); - assert_eq!(result.unwrap(), 1); - - let content = std::fs::read_to_string(¬e).unwrap(); - assert!(content.contains("Archived: true")); - assert!(content.contains("Status: Active")); - } - - #[test] - fn test_batch_trash_notes() { - let dir = tempfile::TempDir::new().unwrap(); - let note = dir.path().join("note.md"); - std::fs::write(¬e, "---\nStatus: Active\n---\n# Note\n").unwrap(); - - let result = batch_trash_notes(vec![note.to_str().unwrap().to_string()]); - assert_eq!(result.unwrap(), 1); - - let content = std::fs::read_to_string(¬e).unwrap(); - assert!(content.contains("Trashed: true")); - assert!(content.contains("Trashed at")); - } - - #[test] - fn test_reload_vault_entry_reads_from_disk() { - let dir = tempfile::TempDir::new().unwrap(); - let note = dir.path().join("test.md"); - std::fs::write(¬e, "---\ntitle: Test\nStatus: Active\n---\n# Test\n").unwrap(); - - let entry = reload_vault_entry(note.to_str().unwrap().to_string()).unwrap(); - assert_eq!(entry.title, "Test"); - assert_eq!(entry.status, Some("Active".to_string())); - - // Modify file on disk - std::fs::write(¬e, "---\ntitle: Test\nStatus: Done\n---\n# Test\n").unwrap(); - let fresh = reload_vault_entry(note.to_str().unwrap().to_string()).unwrap(); - assert_eq!(fresh.status, Some("Done".to_string())); - } - - #[test] - fn test_reload_vault_entry_nonexistent() { - let result = reload_vault_entry("/nonexistent/path.md".to_string()); - assert!(result.is_err()); - } - - #[test] - fn test_reload_vault_invalidates_cache_and_rescans() { - let dir = tempfile::TempDir::new().unwrap(); - let vault_path = dir.path(); - // Init git repo for caching to work - std::process::Command::new("git") - .args(["init"]) - .current_dir(vault_path) - .output() - .unwrap(); - std::process::Command::new("git") - .args(["config", "user.email", "t@t.com"]) - .current_dir(vault_path) - .output() - .unwrap(); - std::process::Command::new("git") - .args(["config", "user.name", "T"]) - .current_dir(vault_path) - .output() - .unwrap(); - - // Set test cache dir to avoid polluting real cache - let cache_dir = tempfile::TempDir::new().unwrap(); - std::env::set_var( - "LAPUTA_CACHE_DIR", - cache_dir.path().to_string_lossy().as_ref(), - ); - - std::fs::write( - vault_path.join("note.md"), - "---\nTrashed: false\n---\n# Note\n", - ) - .unwrap(); - std::process::Command::new("git") - .args(["add", "."]) - .current_dir(vault_path) - .output() - .unwrap(); - std::process::Command::new("git") - .args(["commit", "-m", "init"]) - .current_dir(vault_path) - .output() - .unwrap(); - - // Prime cache via list_vault - let entries = list_vault(vault_path.to_str().unwrap().to_string()).unwrap(); - assert!(!entries[0].trashed); - - // Trash the note on disk - std::fs::write( - vault_path.join("note.md"), - "---\nTrashed: true\n---\n# Note\n", - ) - .unwrap(); - - // reload_vault must return the updated trashed state - let vp_str = vault_path.to_str().unwrap(); - crate::vault::invalidate_cache(std::path::Path::new(vp_str)); - let fresh = crate::vault::scan_vault_cached(std::path::Path::new(vp_str)).unwrap(); - assert!( - fresh[0].trashed, - "reload_vault must reflect disk state after trashing" - ); - } - - #[test] - fn test_check_vault_exists_false() { - assert!(!check_vault_exists("/nonexistent/path/abc123".to_string())); - } - - #[test] - fn test_get_default_vault_path_returns_ok() { - let result = get_default_vault_path(); - assert!(result.is_ok()); - } - - #[test] - fn test_repair_vault_flattens_type_folders() { - let dir = tempfile::TempDir::new().unwrap(); - let vault_path = dir.path(); - let note_dir = vault_path.join("note"); - std::fs::create_dir_all(¬e_dir).unwrap(); - std::fs::write( - note_dir.join("hello.md"), - "---\nis_a: Note\n---\n# Hello\n", - ) - .unwrap(); - - let result = repair_vault(vault_path.to_str().unwrap().to_string()); - assert!(result.is_ok()); - // Note moved from note/ subfolder to root - assert!(vault_path.join("hello.md").exists()); - assert!(!note_dir.join("hello.md").exists()); - // Legacy is_a migrated to type - let content = std::fs::read_to_string(vault_path.join("hello.md")).unwrap(); - assert!(content.contains("type: Note")); - assert!(!content.contains("is_a:")); - } - - #[test] - fn test_repair_vault_creates_config_files() { - let dir = tempfile::TempDir::new().unwrap(); - let vault_path = dir.path(); - - let result = repair_vault(vault_path.to_str().unwrap().to_string()); - assert!(result.is_ok()); - // Config files at root - assert!(vault_path.join("AGENTS.md").exists()); - assert!(vault_path.join("config.md").exists()); - // .gitignore - assert!(vault_path.join(".gitignore").exists()); - } } diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index ca318503..e4f9e801 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -1,8 +1,8 @@ #[cfg(desktop)] use crate::menu; use crate::settings::Settings; +use crate::vault_list; use crate::vault_list::VaultList; -use crate::{vault_list}; use super::parse_build_label; diff --git a/src-tauri/src/commands/vault.rs b/src-tauri/src/commands/vault.rs index 1d5b8ff8..be279d24 100644 --- a/src-tauri/src/commands/vault.rs +++ b/src-tauri/src/commands/vault.rs @@ -211,3 +211,161 @@ pub fn repair_vault(vault_path: String) -> Result { git::ensure_gitignore(&vault_path)?; Ok("Vault repaired".to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_note(body: &str) -> (tempfile::TempDir, std::path::PathBuf) { + let dir = tempfile::TempDir::new().unwrap(); + let note = dir.path().join("note.md"); + std::fs::write(¬e, body).unwrap(); + (dir, note) + } + + #[test] + fn test_batch_archive_notes() { + let (_dir, note) = temp_note("---\nStatus: Active\n---\n# Note\n"); + assert_eq!( + batch_archive_notes(vec![note.to_str().unwrap().to_string()]).unwrap(), + 1 + ); + let content = std::fs::read_to_string(¬e).unwrap(); + assert!(content.contains("Archived: true")); + assert!(content.contains("Status: Active")); + } + + #[test] + fn test_batch_trash_notes() { + let (_dir, note) = temp_note("---\nStatus: Active\n---\n# Note\n"); + assert_eq!( + batch_trash_notes(vec![note.to_str().unwrap().to_string()]).unwrap(), + 1 + ); + let content = std::fs::read_to_string(¬e).unwrap(); + assert!(content.contains("Trashed: true")); + assert!(content.contains("Trashed at")); + } + + #[test] + fn test_reload_vault_entry_reads_from_disk() { + let dir = tempfile::TempDir::new().unwrap(); + let note = dir.path().join("test.md"); + std::fs::write(¬e, "---\ntitle: Test\nStatus: Active\n---\n# Test\n").unwrap(); + + let entry = reload_vault_entry(note.to_str().unwrap().to_string()).unwrap(); + assert_eq!(entry.title, "Test"); + assert_eq!(entry.status, Some("Active".to_string())); + + // Modify file on disk + std::fs::write(¬e, "---\ntitle: Test\nStatus: Done\n---\n# Test\n").unwrap(); + let fresh = reload_vault_entry(note.to_str().unwrap().to_string()).unwrap(); + assert_eq!(fresh.status, Some("Done".to_string())); + } + + #[test] + fn test_reload_vault_entry_nonexistent() { + let result = reload_vault_entry("/nonexistent/path.md".to_string()); + assert!(result.is_err()); + } + + #[test] + fn test_reload_vault_invalidates_cache_and_rescans() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path(); + std::process::Command::new("git") + .args(["init"]) + .current_dir(vault_path) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["config", "user.email", "t@t.com"]) + .current_dir(vault_path) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["config", "user.name", "T"]) + .current_dir(vault_path) + .output() + .unwrap(); + + let cache_dir = tempfile::TempDir::new().unwrap(); + std::env::set_var( + "LAPUTA_CACHE_DIR", + cache_dir.path().to_string_lossy().as_ref(), + ); + + std::fs::write( + vault_path.join("note.md"), + "---\nTrashed: false\n---\n# Note\n", + ) + .unwrap(); + std::process::Command::new("git") + .args(["add", "."]) + .current_dir(vault_path) + .output() + .unwrap(); + std::process::Command::new("git") + .args(["commit", "-m", "init"]) + .current_dir(vault_path) + .output() + .unwrap(); + + let entries = list_vault(vault_path.to_str().unwrap().to_string()).unwrap(); + assert!(!entries[0].trashed); + + std::fs::write( + vault_path.join("note.md"), + "---\nTrashed: true\n---\n# Note\n", + ) + .unwrap(); + + let vp_str = vault_path.to_str().unwrap(); + crate::vault::invalidate_cache(std::path::Path::new(vp_str)); + let fresh = crate::vault::scan_vault_cached(std::path::Path::new(vp_str)).unwrap(); + assert!( + fresh[0].trashed, + "reload_vault must reflect disk state after trashing" + ); + } + + #[test] + fn test_check_vault_exists_false() { + assert!(!check_vault_exists("/nonexistent/path/abc123".to_string())); + } + + #[test] + fn test_get_default_vault_path_returns_ok() { + let result = get_default_vault_path(); + assert!(result.is_ok()); + } + + #[test] + fn test_repair_vault_flattens_type_folders() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path(); + let note_dir = vault_path.join("note"); + std::fs::create_dir_all(¬e_dir).unwrap(); + std::fs::write(note_dir.join("hello.md"), "---\nis_a: Note\n---\n# Hello\n").unwrap(); + + let result = repair_vault(vault_path.to_str().unwrap().to_string()); + assert!(result.is_ok()); + assert!(vault_path.join("hello.md").exists()); + assert!(!note_dir.join("hello.md").exists()); + let content = std::fs::read_to_string(vault_path.join("hello.md")).unwrap(); + assert!(content.contains("type: Note")); + assert!(!content.contains("is_a:")); + } + + #[test] + fn test_repair_vault_creates_config_files() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path(); + + let result = repair_vault(vault_path.to_str().unwrap().to_string()); + assert!(result.is_ok()); + assert!(vault_path.join("AGENTS.md").exists()); + assert!(vault_path.join("config.md").exists()); + assert!(vault_path.join(".gitignore").exists()); + } +}