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:
Test
2026-03-29 10:01:29 +02:00
parent a59640634e
commit 0488a3c505
5 changed files with 249 additions and 250 deletions

View File

@@ -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())
}

View 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())
}

View File

@@ -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(&note, "---\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(&note).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(&note, "---\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(&note).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(&note, "---\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(&note, "---\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(&note_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());
}
}

View File

@@ -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;

View File

@@ -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(&note, 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(&note).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(&note).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(&note, "---\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(&note, "---\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(&note_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());
}
}