refactor: extract github.rs from git.rs, move tests to vault.rs
Completes the commands/ module split: GitHub commands now live in github.rs (88 lines) instead of bloating git.rs (318→233). Vault and frontmatter tests moved from mod.rs to vault.rs where they belong (mod.rs 254→93 lines, CodeScene 9.24→9.84). Deduplicated batch_archive/trash test setup with a shared temp_note helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<GitRemoteStatus, S
|
||||
behind: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// ── GitHub commands (desktop) ───────────────────────────────────────────────
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub async fn github_list_repos(token: String) -> Result<Vec<GithubRepo>, 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<GithubRepo, String> {
|
||||
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<String, String> {
|
||||
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<DeviceFlowStart, String> {
|
||||
crate::github::github_device_flow_start().await
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub async fn github_device_flow_poll(device_code: String) -> Result<DeviceFlowPollResult, String> {
|
||||
crate::github::github_device_flow_poll(&device_code).await
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub async fn github_get_user(token: String) -> Result<GitHubUser, String> {
|
||||
crate::github::github_get_user(&token).await
|
||||
}
|
||||
|
||||
// ── GitHub commands (mobile stubs) ──────────────────────────────────────────
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_list_repos(_token: String) -> Result<Vec<GithubRepo>, 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<GithubRepo, String> {
|
||||
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<String, String> {
|
||||
Err("Git clone is not available on mobile".into())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_device_flow_start() -> Result<DeviceFlowStart, String> {
|
||||
Err("GitHub integration is not available on mobile".into())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_device_flow_poll(_device_code: String) -> Result<DeviceFlowPollResult, String> {
|
||||
Err("GitHub integration is not available on mobile".into())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_get_user(_token: String) -> Result<GitHubUser, String> {
|
||||
Err("GitHub integration is not available on mobile".into())
|
||||
}
|
||||
|
||||
88
src-tauri/src/commands/github.rs
Normal file
88
src-tauri/src/commands/github.rs
Normal file
@@ -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<Vec<GithubRepo>, 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<GithubRepo, String> {
|
||||
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<String, String> {
|
||||
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<DeviceFlowStart, String> {
|
||||
crate::github::github_device_flow_start().await
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub async fn github_device_flow_poll(device_code: String) -> Result<DeviceFlowPollResult, String> {
|
||||
crate::github::github_device_flow_poll(&device_code).await
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub async fn github_get_user(token: String) -> Result<GitHubUser, String> {
|
||||
crate::github::github_get_user(&token).await
|
||||
}
|
||||
|
||||
// ── GitHub commands (mobile stubs) ──────────────────────────────────────────
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_list_repos(_token: String) -> Result<Vec<GithubRepo>, 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<GithubRepo, String> {
|
||||
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<String, String> {
|
||||
Err("Git clone is not available on mobile".into())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_device_flow_start() -> Result<DeviceFlowStart, String> {
|
||||
Err("GitHub integration is not available on mobile".into())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_device_flow_poll(_device_code: String) -> Result<DeviceFlowPollResult, String> {
|
||||
Err("GitHub integration is not available on mobile".into())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub async fn github_get_user(_token: String) -> Result<GitHubUser, String> {
|
||||
Err("GitHub integration is not available on mobile".into())
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -211,3 +211,161 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user