weavatrix-worktree 0.2.1

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

use super::TargetAccess;
use crate::{
    filesystem::{FsRoot, PresentEvidence, SlotEvidence, SlotProbe, SlotSnapshot},
    hash::Sha256Hash,
};

fn stage(access: &TargetAccess, name: &str, bytes: &[u8]) {
    let mut file = access.create_new(name).unwrap();
    file.write_all(bytes).unwrap();
    file.sync_all().unwrap();
}

#[test]
fn probes_absence_and_installs_without_replacement() {
    let temp = tempfile::tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("new.rs").unwrap();
    assert_eq!(access.probe_slot().unwrap(), SlotProbe::Absent);
    stage(&access, ".weavatrix-new.stage", b"new\n");

    access.install_absent_from(".weavatrix-new.stage").unwrap();

    assert_eq!(std::fs::read(temp.path().join("new.rs")).unwrap(), b"new\n");
    assert!(!temp.path().join(".weavatrix-new.stage").exists());
    let SlotProbe::Present(probe) = access.probe_slot().unwrap() else {
        panic!("installed target is absent");
    };
    assert_eq!(probe.bytes, 4);
}

#[test]
fn absent_install_never_clobbers_a_competing_destination() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join("new.rs"), "external\n").unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("new.rs").unwrap();
    stage(&access, ".weavatrix-new.stage", b"planned\n");

    let error = access
        .install_absent_from(".weavatrix-new.stage")
        .unwrap_err();

    assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
    assert_eq!(
        std::fs::read(temp.path().join("new.rs")).unwrap(),
        b"external\n"
    );
    assert!(temp.path().join(".weavatrix-new.stage").exists());
}

#[test]
fn exact_state_includes_hash_identity_size_and_permissions() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join("file.rs"), "old\n").unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("file.rs").unwrap();
    let SlotSnapshot::Present(snapshot) = access.snapshot_slot(1024).unwrap() else {
        panic!("target is absent");
    };
    let expected = PresentEvidence {
        sha256: Sha256Hash::compute(&snapshot.source),
        bytes: snapshot.source.len() as u64,
        identity: snapshot.identity,
        permissions: snapshot.portable_permissions,
    };

    assert_eq!(
        access
            .verify_slot(SlotEvidence::Present(expected), 1024)
            .unwrap(),
        SlotEvidence::Present(expected)
    );
    std::fs::write(temp.path().join("file.rs"), "new\n").unwrap();
    assert!(
        access
            .verify_slot(SlotEvidence::Present(expected), 1024)
            .is_err()
    );
}

#[test]
fn recognizes_the_two_link_install_crash_intermediate() {
    let temp = tempfile::tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("new.rs").unwrap();
    stage(&access, ".weavatrix-linked.stage", b"new\n");
    access
        .parent
        .hard_link(".weavatrix-linked.stage", &access.parent, "new.rs")
        .unwrap();

    assert!(
        access
            .same_file_as_artifact(".weavatrix-linked.stage")
            .unwrap()
    );
    let identity = access
        .safe_artifact_identity(".weavatrix-linked.stage", 2)
        .unwrap();
    access
        .finish_linked_install(".weavatrix-linked.stage", identity)
        .unwrap();
    assert!(!temp.path().join(".weavatrix-linked.stage").exists());
    assert!(temp.path().join("new.rs").exists());
}

#[test]
fn reverses_the_two_link_install_crash_intermediate() {
    let temp = tempfile::tempdir().unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("new.rs").unwrap();
    stage(&access, ".weavatrix-linked.stage", b"new\n");
    access
        .parent
        .hard_link(".weavatrix-linked.stage", &access.parent, "new.rs")
        .unwrap();

    access
        .rollback_linked_install(".weavatrix-linked.stage")
        .unwrap();

    assert!(!temp.path().join("new.rs").exists());
    assert!(temp.path().join(".weavatrix-linked.stage").exists());
}

#[test]
fn root_revalidates_the_original_parent_identity() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::create_dir(temp.path().join("src")).unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("src/new.rs").unwrap();

    root.revalidate_parent(&access).unwrap();
    assert_eq!(
        access.parent_identity(),
        access.verify_parent_handle().unwrap()
    );
}

#[test]
fn permission_and_slot_evidence_round_trip() {
    let temp = tempfile::tempdir().unwrap();
    std::fs::write(temp.path().join("file.rs"), "old\n").unwrap();
    let root = FsRoot::open(temp.path()).unwrap();
    let access = root.open_target("file.rs").unwrap();
    let SlotSnapshot::Present(snapshot) = access.snapshot_slot(1024).unwrap() else {
        panic!("target is absent");
    };
    let evidence = SlotEvidence::Present(PresentEvidence {
        sha256: Sha256Hash::compute(&snapshot.source),
        bytes: snapshot.source.len() as u64,
        identity: snapshot.identity,
        permissions: snapshot.portable_permissions,
    });

    let encoded = blazingly_json::to_vec(&evidence).unwrap();
    let decoded: SlotEvidence = blazingly_json::from_slice(&encoded).unwrap();
    assert_eq!(decoded, evidence);
}