weavatrix-worktree 0.2.0

Bounded, crash-recoverable multi-file worktree edits for source refactoring
Documentation
use std::fs;

use tempfile::{TempDir, tempdir};

use crate::{
    DeleteFile, RecoveryAction, Sha256Hash, WorktreeOperation, WorktreeOptions, WorktreePlan,
    filesystem::{FsRoot, SlotEvidence},
    operation::{
        PreparedWorktreeTransaction, journal::Record, prepare_operation_plan,
        recover_operation_transaction,
    },
};

fn prepare_linked_delete_restore(
    temp: &TempDir,
    root: &FsRoot,
    options: WorktreeOptions,
) -> PreparedWorktreeTransaction {
    fs::write(temp.path().join("old.rs"), "old").unwrap();
    let plan = WorktreePlan::new(
        "delete",
        vec![WorktreeOperation::Delete(DeleteFile::new(
            "old.rs",
            Sha256Hash::compute(b"old").to_string(),
        ))],
    );
    let mut prepared = prepare_operation_plan(root, options, &plan).unwrap();
    let path = &prepared.paths[0];
    let index = path.stable_index;
    let SlotEvidence::Present(before) = path.before else {
        panic!("delete input is absent");
    };
    let backup = path.backup_name.clone().unwrap();
    prepared
        .journal
        .append(&Record::CommitIntent { index })
        .unwrap();
    prepared.paths[0]
        .access
        .remove_exact(before, options.limits.max_source_bytes_per_file)
        .unwrap();
    prepared
        .journal
        .append(&Record::Committed { index })
        .unwrap();
    prepared
        .journal
        .append(&Record::RollbackIntent { index })
        .unwrap();
    prepared.paths[0].access.link_absent_from(&backup).unwrap();
    prepared
}

#[test]
fn recovery_finishes_a_two_link_delete_backup_restore() {
    let temp = tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let options = WorktreeOptions::default();
    let prepared = prepare_linked_delete_restore(&temp, &root, options);
    drop(prepared);

    let report = recover_operation_transaction(&root, options)
        .unwrap()
        .unwrap();

    assert_eq!(report.action(), RecoveryAction::RolledBack);
    assert_eq!(
        fs::read_to_string(temp.path().join("old.rs")).unwrap(),
        "old"
    );
    assert!(!journal_exists(&temp));
}

#[test]
fn recovery_replays_after_linked_backup_finalization() {
    let temp = tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let options = WorktreeOptions::default();
    let prepared = prepare_linked_delete_restore(&temp, &root, options);
    let path = &prepared.paths[0];
    let backup = path.backup_name.as_deref().unwrap();
    let identity = path.backup.unwrap().identity;
    path.access.finish_linked_install(backup, identity).unwrap();
    drop(prepared);

    let report = recover_operation_transaction(&root, options)
        .unwrap()
        .unwrap();

    assert_eq!(report.action(), RecoveryAction::RolledBack);
    assert_eq!(
        fs::read_to_string(temp.path().join("old.rs")).unwrap(),
        "old"
    );
    assert!(!journal_exists(&temp));
}

#[test]
fn recovery_refuses_a_tampered_two_link_delete_backup_restore() {
    let temp = tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let options = WorktreeOptions::default();
    let prepared = prepare_linked_delete_restore(&temp, &root, options);
    let backup = prepared.paths[0].backup_name.clone().unwrap();
    fs::write(temp.path().join(backup), "tampered").unwrap();
    drop(prepared);

    let error = recover_operation_transaction(&root, options).unwrap_err();

    assert!(error.recovery_required());
    assert_eq!(
        fs::read_to_string(temp.path().join("old.rs")).unwrap(),
        "tampered"
    );
}

#[test]
fn recovery_refuses_a_foreign_target_during_linked_delete_restore() {
    let temp = tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let options = WorktreeOptions::default();
    let prepared = prepare_linked_delete_restore(&temp, &root, options);
    fs::remove_file(temp.path().join("old.rs")).unwrap();
    fs::write(temp.path().join("old.rs"), "external").unwrap();
    drop(prepared);

    let error = recover_operation_transaction(&root, options).unwrap_err();

    assert!(error.recovery_required());
    assert_eq!(
        fs::read_to_string(temp.path().join("old.rs")).unwrap(),
        "external"
    );
}

fn journal_exists(temp: &TempDir) -> bool {
    temp.path()
        .join(".weavatrix/worktree/active-v3.jsonl")
        .exists()
}