f3f1f51d9e
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
160 lines
7.6 KiB
Rust
160 lines
7.6 KiB
Rust
//! Git commands dispatch through the registry and synchronize notes safely.
|
|
|
|
mod common;
|
|
|
|
use common::{on_disk, seed, temp_config, temp_vault};
|
|
use notescraft_engine::{AppSettings, Error, Registry, Session, adversarial_params, register_core};
|
|
use serde_json::{Value, json};
|
|
use std::process::Command;
|
|
|
|
const IDS: &[&str] =
|
|
&["git.init", "git.status", "git.diff", "git.stage", "git.unstage", "git.restore", "git.commit", "git.log", "git.show", "git.restore_version"];
|
|
const WRITES: &[&str] = &["git.init", "git.stage", "git.unstage", "git.restore", "git.commit", "git.restore_version"];
|
|
|
|
fn available() -> bool {
|
|
if Command::new("git").arg("--version").output().is_ok_and(|out| out.status.success()) {
|
|
return true;
|
|
}
|
|
eprintln!("git missing: skipping real git test");
|
|
false
|
|
}
|
|
|
|
fn run(reg: &Registry<Session>, session: &mut Session, id: &str, params: &Value) -> Value {
|
|
reg.run(id, session, params).unwrap_or_else(|error| panic!("{id}: {error}"))
|
|
}
|
|
|
|
fn identity(session: &Session) {
|
|
for (key, value) in [("user.email", "test@example.invalid"), ("user.name", "NotesCraft test")] {
|
|
assert!(Command::new("git").args(["config", key, value]).current_dir(session.vault().root()).status().expect("config").success());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_git_command_round_trips_and_restore_reindexes() {
|
|
if !available() {
|
|
return;
|
|
}
|
|
let vault = temp_vault("git-commands");
|
|
seed(vault.path(), &[("note.md", "# Original\n")]);
|
|
let mut session = Session::open(vault.path()).expect("session");
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register");
|
|
run(®, &mut session, "git.init", &Value::Null);
|
|
assert!(session.git().is_repo());
|
|
identity(&session);
|
|
run(®, &mut session, "git.status", &Value::Null);
|
|
run(®, &mut session, "git.diff", &json!({"path":"note.md"}));
|
|
run(®, &mut session, "git.stage", &json!({"paths":["note.md"]}));
|
|
run(®, &mut session, "git.diff", &json!({"path":"note.md", "staged":true}));
|
|
run(®, &mut session, "git.unstage", &json!({"paths":["note.md"]}));
|
|
run(®, &mut session, "git.stage", &json!({"paths":["note.md"]}));
|
|
run(®, &mut session, "git.commit", &json!({"message":"original"}));
|
|
seed(vault.path(), &[("note.md", "# Changed\n")]);
|
|
session.apply_event(notescraft_vault::VaultEvent::Modified("note.md".into())).expect("update index");
|
|
session.open_buffer("note.md").expect("buffer");
|
|
run(®, &mut session, "git.restore", &json!({"paths":["note.md"]}));
|
|
assert_eq!(on_disk(vault.path(), "note.md"), "# Original\n");
|
|
assert_eq!(session.buffer("note.md").expect("buffer").text(), "# Original\n");
|
|
assert_eq!(session.search("Original", 10).expect("search").len(), 1);
|
|
assert_eq!(session.search("Changed", 10).expect("search"), []);
|
|
seed(vault.path(), &[("note.md", "# Second\n")]);
|
|
run(®, &mut session, "git.stage", &json!({"paths":["note.md"]}));
|
|
run(®, &mut session, "git.commit", &json!({"message":"second"}));
|
|
run(®, &mut session, "git.log", &json!({"limit":10,"path":"note.md"}));
|
|
let shown = run(®, &mut session, "git.show", &json!({"rev":"HEAD~1","path":"note.md"}));
|
|
assert_eq!(shown.get("content"), Some(&json!("# Original\n")));
|
|
run(®, &mut session, "git.restore_version", &json!({"rev":"HEAD~1","path":"note.md"}));
|
|
assert_eq!(session.search("Original", 10).expect("search").len(), 1);
|
|
assert_eq!(session.search("Second", 10).expect("search"), []);
|
|
for id in IDS {
|
|
let spec = reg.get(id).expect("registered");
|
|
assert_ne!(spec.doc, "");
|
|
assert!(spec.params.iter().all(|param| !param.doc.is_empty()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn readonly_gates_writes_and_all_git_commands_survive_adversarial_params() {
|
|
if !available() {
|
|
return;
|
|
}
|
|
let vault = temp_vault("git-adversarial");
|
|
let config = temp_config("git-adversarial");
|
|
let mut session = Session::open(vault.path()).expect("session");
|
|
session.use_settings_dir(config.path());
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register");
|
|
run(®, &mut session, "git.init", &Value::Null);
|
|
seed(vault.path(), &[("note.md", "# Readonly\n")]);
|
|
identity(&session);
|
|
run(®, &mut session, "git.stage", &json!({"paths":["note.md"]}));
|
|
run(®, &mut session, "git.commit", &json!({"message":"readonly fixture"}));
|
|
for id in IDS {
|
|
for params in adversarial_params() {
|
|
assert!(!matches!(reg.run(id, &mut session, ¶ms), Err(Error::Panicked(_))), "{id}");
|
|
}
|
|
}
|
|
let mut readonly = Session::open_read_only(vault.path()).expect("readonly");
|
|
for id in WRITES {
|
|
assert!(matches!(reg.run(id, &mut readonly, &Value::Null), Err(Error::Disabled(_))), "{id}");
|
|
}
|
|
for id in ["git.status", "git.diff", "git.log", "git.show"] {
|
|
assert!(reg.is_enabled(id, &readonly));
|
|
}
|
|
run(®, &mut readonly, "git.status", &Value::Null);
|
|
run(®, &mut readonly, "git.log", &Value::Null);
|
|
run(®, &mut readonly, "git.diff", &json!({"path":"note.md"}));
|
|
run(®, &mut readonly, "git.show", &json!({"rev":"HEAD","path":"note.md"}));
|
|
for params in [json!({"paths":["../escape"]}), json!({"paths":[1]}), json!({"paths":[".obsidian/config"]})] {
|
|
assert!(reg.run("git.stage", &mut session, ¶ms).is_err());
|
|
}
|
|
assert!(reg.run("git.log", &mut session, &json!({"limit":-1})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn restoring_records_dirty_conflict_without_overwriting_buffer() {
|
|
if !available() {
|
|
return;
|
|
}
|
|
let vault = temp_vault("git-dirty");
|
|
seed(vault.path(), &[("note.md", "# Original\n")]);
|
|
let mut session = Session::open(vault.path()).expect("session");
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("register");
|
|
run(®, &mut session, "git.init", &Value::Null);
|
|
identity(&session);
|
|
run(®, &mut session, "git.stage", &json!({"paths":["note.md"]}));
|
|
run(®, &mut session, "git.commit", &json!({"message":"original"}));
|
|
session.open_buffer("note.md").expect("open");
|
|
session.buffer_mut("note.md").expect("buffer").edit(0..0, "dirty ").expect("edit");
|
|
seed(vault.path(), &[("note.md", "# External\n")]);
|
|
run(®, &mut session, "git.restore", &json!({"paths":["note.md"]}));
|
|
assert_eq!(session.buffer("note.md").expect("buffer").text(), "dirty # Original\n");
|
|
assert_eq!(session.conflicts(), &["note.md"]);
|
|
run(®, &mut session, "git.restore_version", &json!({"rev":"HEAD","path":"note.md"}));
|
|
assert_eq!(session.buffer("note.md").expect("buffer").text(), "dirty # Original\n");
|
|
}
|
|
|
|
#[test]
|
|
fn auto_commit_defaults_off_and_clamps_and_persists() {
|
|
let mut settings = AppSettings::default();
|
|
assert_eq!(settings.git.auto_commit_minutes, 0);
|
|
settings.git.auto_commit_minutes = 999;
|
|
assert!(settings.validate().iter().any(|note| note.contains("git.auto_commit_minutes")));
|
|
assert_eq!(settings.git.auto_commit_minutes, 240);
|
|
let config = temp_config("git-settings");
|
|
notescraft_engine::save_at(config.path(), &settings).expect("save");
|
|
let (loaded, _) = notescraft_engine::load_at(config.path());
|
|
assert_eq!(loaded.git, settings.git);
|
|
settings.git.auto_commit_minutes = -1;
|
|
assert_eq!(settings.validate().len(), 1);
|
|
assert_eq!(settings.git.auto_commit_minutes, 0);
|
|
let vault = temp_vault("git-settings");
|
|
let mut session = Session::open(vault.path()).expect("session");
|
|
session.use_settings_dir(config.path());
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("registry");
|
|
run(®istry, &mut session, "settings.set", &json!({"key":"git.auto_commit_minutes", "value":15}));
|
|
assert_eq!(notescraft_engine::load_at(config.path()).0.git.auto_commit_minutes, 15);
|
|
}
|