patchloom 0.7.0

Structured file editing library and CLI for AI agents: parser-backed JSON/YAML/TOML edits, AST-aware code operations, multi-file batching, markdown operations, and MCP server
Documentation
use super::*;

#[test]
fn test_init_creates_agents_md_in_empty_dir() {
    let dir = TempDir::new().unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .output()
        .unwrap();
    assert!(output.status.success());
    let agents = dir.path().join("AGENTS.md");
    assert!(agents.exists(), "AGENTS.md should be created");
    let content = fs::read_to_string(&agents).unwrap();
    assert!(content.contains("patchloom"));
    assert!(content.contains("# Patchloom"));
}

#[test]
fn test_init_appends_to_existing_agents_md() {
    let dir = TempDir::new().unwrap();
    fs::write(dir.path().join("AGENTS.md"), "# My Rules\n").unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .output()
        .unwrap();
    assert!(output.status.success());
    let content = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert!(
        content.starts_with("# My Rules\n"),
        "original content should be preserved"
    );
    assert!(
        content.contains("# Patchloom"),
        "patchloom rules should be appended"
    );
}

#[test]
fn test_init_appends_when_existing_agents_mentions_patchloom_without_generated_header() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("AGENTS.md"),
        "# Rules\nUse patchloom for edits.\n",
    )
    .unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .env("SHELL", "unknown")
        .output()
        .unwrap();
    assert!(output.status.success());
    let content = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert!(content.starts_with("# Rules\nUse patchloom for edits.\n"));
    assert!(content.contains("# Patchloom"));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("appended patchloom rules"));
}

#[test]
fn test_init_skips_if_patchloom_already_present() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("AGENTS.md"),
        "# Rules\n<!-- Generated by patchloom v0.1.0 -->\n",
    )
    .unwrap();
    let before = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .output()
        .unwrap();
    assert!(output.status.success());
    let after = fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
    assert_eq!(before, after, "file should not be modified");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("already contains patchloom rules"));
}

#[test]
fn test_init_errors_on_non_utf8_existing_agents_md() {
    let dir = TempDir::new().unwrap();
    let agents = dir.path().join("AGENTS.md");
    fs::write(&agents, [0xff, 0xfe, 0x00]).unwrap();
    let before = fs::read(&agents).unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .env("SHELL", "unknown")
        .output()
        .unwrap();
    assert!(!output.status.success());
    assert_eq!(
        fs::read(&agents).unwrap(),
        before,
        "file should not be modified"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("reading existing"));
}

#[test]
fn test_init_appends_to_existing_claude_md() {
    let dir = TempDir::new().unwrap();
    let claude = dir.path().join("Claude.md");
    fs::write(&claude, "# Claude Rules\n").unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .env("SHELL", "unknown")
        .output()
        .unwrap();
    assert!(output.status.success());
    assert!(!dir.path().join("AGENTS.md").exists());
    let content = fs::read_to_string(&claude).unwrap();
    assert!(content.starts_with("# Claude Rules\n"));
    assert!(content.contains("# Patchloom"));
}

#[test]
fn test_init_quiet_suppresses_output() {
    let dir = TempDir::new().unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--quiet", "--cwd"])
        .arg(dir.path())
        .output()
        .unwrap();
    assert!(output.status.success());
    // File should still be created
    assert!(dir.path().join("AGENTS.md").exists());
    // But stderr should be empty
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.is_empty(), "stderr should be empty with --quiet");
}

#[test]
fn test_init_falls_back_to_completion_command_when_completion_dir_creation_fails() {
    let dir = TempDir::new().unwrap();
    let fake_home = dir.path().join("fake-home");
    let blocking_file = fake_home.join(".config");
    fs::create_dir_all(&fake_home).unwrap();
    fs::write(&blocking_file, "not a directory\n").unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .env("HOME", &fake_home)
        .env("SHELL", "fish")
        .output()
        .unwrap();
    assert!(output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("failed to prepare completion directory:"));
    assert!(
        stderr.contains("patchloom completions fish > ~/.config/fish/completions/patchloom.fish")
    );
}

#[cfg(unix)]
#[test]
fn test_init_confirm_eof_skips_agents_creation() {
    let dir = TempDir::new().unwrap();
    let output = run_patchloom_confirm_in_pty_with_env(
        &["init", "--cwd", dir.path().to_str().unwrap()],
        "\u{4}",
        &[("SHELL", "unknown")],
    );

    assert!(output.status.success());
    assert!(!dir.path().join("AGENTS.md").exists());

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Create AGENTS.md? [Y/n]"));
    assert!(stdout.contains("skipped AGENTS.md"));
}

#[cfg(feature = "mcp")]
#[test]
fn test_init_shows_vscode_mcp_json_hint() {
    if !has_mcp_support() {
        return;
    }

    let dir = TempDir::new().unwrap();
    fs::create_dir(dir.path().join(".vscode")).unwrap();
    let home = TempDir::new().unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .env("HOME", home.path())
        .env("SHELL", "unknown")
        .output()
        .unwrap();

    assert!(output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("VS Code: create .vscode/mcp.json:"));
    assert!(stderr.contains(
        "\"servers\": { \"patchloom\": { \"command\": \"patchloom\", \"args\": [\"mcp-server\"] } }"
    ));
    assert!(!stderr.contains(".vscode/settings.json"));
}

#[cfg(feature = "mcp")]
#[test]
fn test_init_shows_cursor_mcp_json_hint() {
    if !has_mcp_support() {
        return;
    }

    let dir = TempDir::new().unwrap();
    fs::create_dir(dir.path().join(".cursor")).unwrap();
    let home = TempDir::new().unwrap();
    let output = Command::cargo_bin("patchloom")
        .unwrap()
        .args(["init", "--yes", "--cwd"])
        .arg(dir.path())
        .env("HOME", home.path())
        .env("SHELL", "unknown")
        .output()
        .unwrap();

    assert!(output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("Cursor: create .cursor/mcp.json:"));
    assert!(stderr.contains(
        "\"servers\": { \"patchloom\": { \"command\": \"patchloom\", \"args\": [\"mcp-server\"] } }"
    ));
}

// ---------------------------------------------------------------------------
// --glob flag
// ---------------------------------------------------------------------------