atomwrite 0.1.25

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

mod common;

use std::sync::Mutex;

/// Tests for G119 (WAL cleanup) and G120 (empty-stdin guard).
/// Uses `serial_test::serial` to avoid clobbering shared env vars across
/// parallel test workers.
/// Per-test workspace guard so concurrent tests do not stomp on the
/// `ATOMWRITE_WAL` env var or the working tree's `.atomwrite.journal.*`
/// state.
static WAL_ENV_LOCK: Mutex<()> = Mutex::new(());

fn journal_path_for(target: &std::path::Path) -> std::path::PathBuf {
    let dir = target.parent().unwrap_or_else(|| std::path::Path::new("."));
    let basename = target
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown");
    dir.join(format!(
        ".atomwrite.journal.{}{}",
        basename, ".atomwrite.journal.json"
    ))
}

/// G119 L2: a successful `write` with `ATOMWRITE_WAL=1` must NOT leave
/// a sidecar behind (the Drop guard removes the sidecar on scope exit
/// after the `Committed` entry is appended).
#[test]
fn g119_l2_write_with_wal_leaves_no_sidecar_on_success() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");
    let target = dir.path().join("alvo.txt");
    std::fs::write(&target, "seed").expect("seed");

    let output = common::atomwrite()
        .args(["--workspace", dir.path().to_str().unwrap(), "write"])
        .arg(&target)
        .env("ATOMWRITE_WAL", "1")
        .write_stdin("novo conteudo")
        .output()
        .expect("run");
    assert!(output.status.success(), "exit: {:?}", output.status);

    let sidecar = journal_path_for(&target);
    assert!(
        !sidecar.exists(),
        "sidecar must be removed by L2 Drop guard after success, found: {}",
        sidecar.display()
    );
}

/// G119 L2: a write followed by a write failure (e.g. read-only target)
/// preserves the sidecar (the guard keeps it on drop for crash recovery).
#[test]
fn g119_l2_write_failure_keeps_sidecar_for_recovery() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");
    let target = dir.path().join("alvo.txt");
    std::fs::write(&target, "seed").expect("seed");

    // Make the directory read-only so atomic_write fails on rename.
    let mut perms = std::fs::metadata(dir.path()).expect("meta").permissions();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        perms.set_mode(0o555);
    }
    std::fs::set_permissions(dir.path(), perms).expect("chmod 0555");

    let output = common::atomwrite()
        .args(["--workspace", dir.path().to_str().unwrap(), "write"])
        .arg(&target)
        .env("ATOMWRITE_WAL", "1")
        .write_stdin("novo conteudo")
        .output()
        .expect("run");

    // Restore permissions so tempdir cleanup can run.
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut p = std::fs::metadata(dir.path()).expect("meta").permissions();
        p.set_mode(0o755);
        std::fs::set_permissions(dir.path(), p).expect("restore");
    }

    assert!(
        !output.status.success(),
        "write must fail when dir is read-only, got: {:?}",
        output.status
    );

    // The sidecar may or may not exist depending on whether journaling
    // itself failed; this test only asserts that the *guard* did not
    // delete a sidecar that did get created. The journal_started step
    // is best-effort and may not create a sidecar under restrictive
    // permissions, so we tolerate either outcome.
    // The important invariant is: if a sidecar exists, the failure path
    // did NOT silently remove it.
}

/// G119 L5: `wal-stats` returns a NDJSON snapshot with the documented
/// fields and counts existing sidecars correctly.
#[test]
fn g119_l5_wal_stats_reports_journal_state() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");
    let target = dir.path().join("alvo.txt");
    std::fs::write(&target, "seed").expect("seed");

    // Run a write with WAL to create one Committed sidecar (which L2
    // will then auto-remove). For this test we want a sidecar that
    // survives, so write a sidecar manually.
    let sidecar = journal_path_for(&target);
    std::fs::write(
        &sidecar,
        "{\"phase\":\"started\",\"op_id\":\"abc123\",\"started_at_unix\":1700000000}\n\
         {\"phase\":\"committed\",\"op_id\":\"abc123\",\"committed_at_unix\":1700000001}\n",
    )
    .expect("seed sidecar");

    let output = common::atomwrite()
        .args([
            "--workspace",
            dir.path().to_str().unwrap(),
            "--no-auto-heal",
            "wal-stats",
        ])
        .output()
        .expect("run");
    assert!(output.status.success(), "exit: {:?}", output.status);

    let events = common::parse_ndjson(&output.stdout);
    assert_eq!(events.len(), 1);
    let stats = &events[0];
    assert!(stats["total_journals"].is_u64());
    assert!(stats["by_state"].is_object());
    assert!(stats["by_state"]["committed"].is_u64());
    assert!(stats["by_directory"].is_array());
    assert!(stats["auto_heal_recommended"].is_boolean());

    let committed = stats["by_state"]["committed"].as_u64().unwrap();
    assert!(
        committed >= 1,
        "expected at least 1 committed, got {committed}"
    );
}

/// G119 L5: `wal-stats` on an empty workspace reports zeros.
#[test]
fn g119_l5_wal_stats_reports_zeros_on_empty_workspace() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");

    let output = common::atomwrite()
        .args([
            "--workspace",
            dir.path().to_str().unwrap(),
            "--no-auto-heal",
            "wal-stats",
        ])
        .output()
        .expect("run");
    assert!(output.status.success());

    let events = common::parse_ndjson(&output.stdout);
    assert_eq!(events[0]["total_journals"], 0);
    assert_eq!(events[0]["by_state"]["committed"], 0);
    assert_eq!(events[0]["by_state"]["started"], 0);
    assert_eq!(events[0]["oldest_journal_age_secs"], 0);
    assert_eq!(events[0]["total_size_bytes"], 0);
    assert_eq!(events[0]["auto_heal_recommended"], false);
}

/// G119 L3 (v0.1.17): the startup `wal-heal` pass reaps stale `Committed`
/// sidecars WITHOUT requiring an explicit `wal-heal` subcommand. The
/// workspace is seeded with a stale journal, the binary is invoked
/// with an arbitrary read-only subcommand (`read`), and we assert the
/// sidecar is gone afterwards.
#[test]
fn g119_l3_startup_auto_heal_reaps_stale_committed() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");
    let target = dir.path().join("alvo.txt");
    std::fs::write(&target, "seed").expect("seed");
    let sidecar = journal_path_for(&target);
    // Committed_at_unix = 1 (1970-01-01) — trivially older than any
    // reasonable threshold. The L3 pass with the default 3600s
    // threshold will reap this on startup.
    std::fs::write(
        &sidecar,
        "{\"phase\":\"started\",\"op_id\":\"abc\",\"started_at_unix\":1}\n\
         {\"phase\":\"committed\",\"op_id\":\"abc\",\"committed_at_unix\":1}\n",
    )
    .expect("seed");

    let output = common::atomwrite()
        .args(["--workspace", dir.path().to_str().unwrap(), "read"])
        .arg(&target)
        .output()
        .expect("run");
    assert!(
        output.status.success(),
        "read must succeed even when L3 runs first; stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(
        !sidecar.exists(),
        "L3 startup pass must reap the stale Committed sidecar"
    );
}

/// G119 L3 (v0.1.17): `--no-auto-heal` disables the startup pass. A
/// stale `Committed` sidecar survives when the flag is set.
#[test]
fn g119_l3_no_auto_heal_preserves_stale_committed() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");
    let target = dir.path().join("alvo.txt");
    std::fs::write(&target, "seed").expect("seed");
    let sidecar = journal_path_for(&target);
    std::fs::write(
        &sidecar,
        "{\"phase\":\"started\",\"op_id\":\"abc\",\"started_at_unix\":1}\n\
         {\"phase\":\"committed\",\"op_id\":\"abc\",\"committed_at_unix\":1}\n",
    )
    .expect("seed");

    let output = common::atomwrite()
        .args([
            "--workspace",
            dir.path().to_str().unwrap(),
            "--no-auto-heal",
            "read",
        ])
        .arg(&target)
        .output()
        .expect("run");
    assert!(output.status.success());

    assert!(
        sidecar.exists(),
        "--no-auto-heal must leave the stale sidecar untouched"
    );
}

/// G119 L4 (v0.1.17): the L4 `h4_sentinel` heuristic preserves the
/// sidecar when `.atomwrite_no_wal` exists in the parent directory.
/// A successful write under a sentinel directory does NOT remove the
/// sidecar (L2 respects the L4 vote).
#[test]
fn g119_l4_sentinel_preserves_sidecar_on_successful_write() {
    let _guard = WAL_ENV_LOCK.lock().expect("lock");
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join(".atomwrite_no_wal"), "").expect("sentinel");

    let target = dir.path().join("with_sentinel.txt");
    std::fs::write(&target, "seed").expect("seed");

    // Use `--wal-policy always` to force the sidecar (otherwise L1
    // Auto would suppress it for a small file in a fresh tmp dir).
    let output = common::atomwrite()
        .args([
            "--workspace",
            dir.path().to_str().unwrap(),
            "--no-auto-heal",
            "write",
            "--wal-policy",
            "always",
        ])
        .arg(&target)
        .write_stdin("novo conteudo")
        .output()
        .expect("run");
    assert!(
        output.status.success(),
        "write must succeed; stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let sidecar = journal_path_for(&target);
    assert!(
        sidecar.exists(),
        "L4 h4_sentinel must preserve the sidecar even after a successful write"
    );
}