moesniper 0.7.10

Escape-proof precision file editor for LLM agents. Hex-encoded content, line-range splicing, atomic writes.
Documentation
use std::fs;
use std::process::Command;
use tempfile::TempDir;

fn sniper() -> Command {
    let mut cmd = Command::new("cargo");
    cmd.args(["run", "--quiet", "--"]);
    cmd
}

#[test]
#[cfg(unix)]
fn test_atomic_write_preserves_permissions() {
    use std::os::unix::fs::PermissionsExt;

    let dir = TempDir::new().unwrap();
    let file_path = dir.path().join("script.sh");
    fs::write(&file_path, "echo 'hello'\n").unwrap();

    let mut perms = fs::metadata(&file_path).unwrap().permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&file_path, perms).unwrap();

    let status = sniper()
        .args([
            file_path.to_str().unwrap(),
            "1",
            "1",
            "6563686f2027776f726c6427",
        ])
        .status()
        .unwrap();

    assert!(status.success());
    assert_eq!(fs::read_to_string(&file_path).unwrap(), "echo 'world'\n");

    let final_perms = fs::metadata(&file_path).unwrap().permissions();
    assert_eq!(final_perms.mode() & 0o777, 0o755);
}

#[test]
fn test_multi_step_undo_stack() {
    let dir = TempDir::new().unwrap();
    let file_path = dir.path().join("stack.txt");
    fs::write(&file_path, "v0\n").unwrap();

    // 5 edits
    for i in 1..=5 {
        let hex = format!("{:02x}", i + 48); // '1', '2', etc.
        let status = sniper()
            .args([file_path.to_str().unwrap(), "1", "1", &hex])
            .status()
            .unwrap();
        assert!(status.success());
    }

    assert_eq!(fs::read_to_string(&file_path).unwrap(), "5\n");

    // 5 undos
    for i in (0..5).rev() {
        let status = sniper()
            .args([file_path.to_str().unwrap(), "--undo"])
            .status()
            .unwrap();
        assert!(status.success());

        let expected = if i == 0 {
            "v0\n".to_string()
        } else {
            format!("{}\n", i)
        };
        assert_eq!(fs::read_to_string(&file_path).unwrap(), expected);
    }

    // 6th undo should fail
    let output = sniper()
        .args([file_path.to_str().unwrap(), "--undo"])
        .output()
        .unwrap();
    assert!(!output.status.success());
}

#[test]
fn test_path_normalization_consistency() {
    let dir = TempDir::new().unwrap();
    let sub = dir.path().join("sub");
    fs::create_dir(&sub).unwrap();
    let file_path = sub.join("norm.txt");
    fs::write(&file_path, "orig\n").unwrap();

    // Edit via relative path
    let status = sniper()
        .args([file_path.to_str().unwrap(), "1", "1", "78"]) // 'x'
        .status()
        .unwrap();
    assert!(status.success());

    // Undo via a different but valid relative path (no parent refs due to security)
    // Just use the same path - the hash-based matching should still work
    let status = sniper()
        .args([file_path.to_str().unwrap(), "--undo"])
        .status()
        .unwrap();
    assert!(status.success());

    assert_eq!(fs::read_to_string(&file_path).unwrap(), "orig\n");
}

#[test]
fn test_encode_stdin_integrity() {
    let input = "special: ' \" \\ \n \t \0";
    let mut child = sniper()
        .args(["encode", "--stdin"])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    use std::io::Write;
    let mut stdin = child
        .stdin
        .take()
        .expect("stdin should be captured by Stdio::piped");
    stdin.write_all(input.as_bytes()).unwrap();
    drop(stdin);

    let output = child.wait_with_output().unwrap();
    let hex = String::from_utf8(output.stdout).unwrap().trim().to_string();

    // Check roundtrip - dummy call to verify it runs
    let _ = sniper()
        .args(["encode", "--stdin"])
        .stdin(std::process::Stdio::piped())
        .status();

    let expected_hex: String = input
        .as_bytes()
        .iter()
        .map(|b| format!("{:02x}", b))
        .collect();
    assert_eq!(hex, expected_hex);
}

#[test]
fn test_splicing_boundaries() {
    let dir = TempDir::new().unwrap();
    let file_path = dir.path().join("boundary.txt");
    fs::write(&file_path, "line1\nline2\nline3\n").unwrap();

    // Insert at start (line 1)
    sniper()
        .args([file_path.to_str().unwrap(), "1", "0", "610a"])
        .status()
        .unwrap(); // "a\n"
    assert_eq!(
        fs::read_to_string(&file_path).unwrap(),
        "a\nline1\nline2\nline3\n"
    );

    // Insert at end (line 5)
    sniper()
        .args([file_path.to_str().unwrap(), "5", "4", "7a"])
        .status()
        .unwrap(); // "z"
    assert_eq!(
        fs::read_to_string(&file_path).unwrap(),
        "a\nline1\nline2\nline3\nz\n"
    ); // it adds newline if original had one
}

#[test]
fn test_unicode_payload() {
    let dir = TempDir::new().unwrap();
    let file_path = dir.path().join("unicode.txt");
    fs::write(&file_path, "empty\n").unwrap();

    // "🦀" in hex is f09fa680
    sniper()
        .args([file_path.to_str().unwrap(), "1", "1", "f09fa680"])
        .status()
        .unwrap();
    assert_eq!(fs::read_to_string(&file_path).unwrap(), "🦀\n");
}

#[test]
fn test_concurrency_locking() {
    use std::sync::{Arc, Barrier};
    use std::thread;

    let dir = Arc::new(TempDir::new().unwrap());
    let file_path = Arc::new(dir.path().join("concurrent.txt"));
    fs::write(&*file_path, "base\n").unwrap();

    let num_threads = 5;
    let barrier = Arc::new(Barrier::new(num_threads));
    let mut handles = vec![];

    for i in 0..num_threads {
        let b = barrier.clone();
        let f = file_path.clone();
        handles.push(thread::spawn(move || {
            b.wait();
            let hex = format!("{:02x}", i + 65); // 'A', 'B', etc.
            let output = Command::new("cargo")
                .args(["run", "--quiet", "--", f.to_str().unwrap(), "1", "1", &hex])
                .output()
                .unwrap();
            output.status.success()
        }));
    }

    let results: Vec<bool> = handles.into_iter().map(|h| h.join().unwrap()).collect();

    // At least one should succeed. Some might fail due to lock timeout (2s)
    // because cargo run is slow, but the file should remain in a valid state.
    assert!(results.iter().any(|&r| r));

    let final_content = fs::read_to_string(&*file_path).unwrap();
    // File must contain exactly one line with a single uppercase letter
    // (one thread won, others failed due to lock contention)
    let trimmed = final_content.trim();
    assert_eq!(
        trimmed.len(),
        1,
        "File must contain exactly one char after concurrent edit, got: {:?}",
        trimmed
    );
    assert!(
        trimmed.chars().next().unwrap().is_ascii_uppercase(),
        "Content must be an uppercase letter (A-E), got: {:?}",
        trimmed
    );
}