link-cli 0.2.7

A CLI tool and reusable library for links manipulation backed by a LiNo-notation doublet storage engine.
Documentation
//! Integration tests for the optional version-control decorator.
//!
//! Mirrors the C# `VersionControlDecoratorTests` and exercises R11–R16
//! of issue #94.

use anyhow::Result;
use link_cli::{
    CommitMode, Link, LogRetentionPolicy, NamedTypesDecorator, TransactionsDecorator,
    VersionControlDecorator, DEFAULT_BRANCH_NAME,
};
use tempfile::NamedTempFile;

struct VcGuard {
    _data_file: NamedTempFile,
    _log_file: NamedTempFile,
    _vc_file: NamedTempFile,
}

fn make_vc() -> (VersionControlDecorator, VcGuard) {
    let data_file = NamedTempFile::new().unwrap();
    let log_file = NamedTempFile::new().unwrap();
    let vc_file = NamedTempFile::new().unwrap();
    let data_links = NamedTypesDecorator::new(data_file.path(), false).unwrap();
    let log_links = NamedTypesDecorator::new(log_file.path(), false).unwrap();
    let vc_links = NamedTypesDecorator::new(vc_file.path(), false).unwrap();
    let tx = TransactionsDecorator::new(
        data_links,
        log_links,
        LogRetentionPolicy::default(),
        CommitMode::default(),
        false,
    )
    .unwrap();
    let vc = VersionControlDecorator::new(tx, vc_links, false).unwrap();
    (
        vc,
        VcGuard {
            _data_file: data_file,
            _log_file: log_file,
            _vc_file: vc_file,
        },
    )
}

#[test]
fn default_branch_exists_on_first_open() {
    let (vc, _guard) = make_vc();
    assert_eq!(DEFAULT_BRANCH_NAME, vc.current_branch());
    let branches = vc.list_branches();
    assert_eq!(1, branches.len());
    assert_eq!(DEFAULT_BRANCH_NAME, branches[0].name);
}

#[test]
fn new_transitions_are_attributed_to_current_branch() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    let _id = vc.create_and_update(0, 0)?;
    let head = vc.transactions().last_logged_sequence();
    assert!(
        head >= 2,
        "create_and_update must produce at least two transitions (got {head})."
    );
    assert_eq!(head, vc.current_sequence());
    Ok(())
}

#[test]
fn checkout_to_zero_rewinds_everything() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    let a = vc.create_and_update(0, 0)?;
    let b = vc.create_and_update(0, 0)?;
    assert!(vc.exists(a));
    assert!(vc.exists(b));

    vc.checkout(0)?;

    assert!(!vc.exists(a), "all links must be rewound after checkout 0");
    assert!(!vc.exists(b));
    assert_eq!(0, vc.current_sequence());
    Ok(())
}

#[test]
fn checkout_and_forward_replay_restores_state() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    let a = vc.create_and_update(0, 0)?;
    let after_first = vc.transactions().last_logged_sequence();
    let b = vc.create_and_update(0, 0)?;
    let after_second = vc.transactions().last_logged_sequence();

    vc.checkout(after_first)?;
    assert!(vc.exists(a), "first link must remain after partial rewind");
    assert!(
        !vc.exists(b),
        "second link must disappear after partial rewind"
    );

    vc.checkout(after_second)?;
    assert!(vc.exists(a));
    assert!(
        vc.exists(b),
        "second link must reappear after forward checkout"
    );
    Ok(())
}

#[test]
fn branch_forks_from_current_head() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    vc.create_and_update(0, 0)?;
    vc.branch("feature", None)?;
    assert!(vc.list_branches().iter().any(|b| b.name == "feature"));
    Ok(())
}

#[test]
fn switch_branch_applies_and_rewinds_transitions() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    let a = vc.create_and_update(0, 0)?;
    let head_before_branch = vc.current_sequence();

    vc.branch("feature", None)?;
    vc.switch_branch("feature")?;
    assert_eq!("feature", vc.current_branch());

    let b = vc.create_and_update(0, 0)?;
    assert!(vc.exists(b));
    let feature_head = vc.current_sequence();

    vc.switch_branch(DEFAULT_BRANCH_NAME)?;
    assert_eq!(DEFAULT_BRANCH_NAME, vc.current_branch());
    assert!(
        vc.exists(a),
        "main-branch link must remain after switching back"
    );
    assert!(
        !vc.exists(b),
        "feature-branch link must disappear after switching back to main"
    );
    assert_eq!(head_before_branch, vc.current_sequence());

    vc.switch_branch("feature")?;
    assert!(vc.exists(a));
    assert!(vc.exists(b), "feature-branch link must reappear");
    assert_eq!(feature_head, vc.current_sequence());
    Ok(())
}

#[test]
fn tag_points_to_current_head() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    vc.create_and_update(0, 0)?;
    vc.tag("v1", None)?;
    let seq = vc.try_get_tag("v1").expect("tag must be retrievable");
    assert_eq!(vc.current_sequence(), seq);
    assert!(vc.list_tags().contains_key("v1"));
    Ok(())
}

#[test]
fn branch_from_explicit_seq_uses_given_point() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    vc.create_and_update(0, 0)?;
    let first_head = vc.current_sequence();
    vc.create_and_update(0, 0)?;

    vc.branch("backport", Some(first_head))?;
    let branches = vc.list_branches();
    let branch = branches
        .iter()
        .find(|b| b.name == "backport")
        .expect("backport branch must exist");
    assert_eq!(first_head, branch.fork_seq);
    Ok(())
}

#[test]
fn recover_rebuilds_state_from_branches_store() -> Result<()> {
    let data_file = NamedTempFile::new()?;
    let log_file = NamedTempFile::new()?;
    let vc_file = NamedTempFile::new()?;
    let data_path = data_file.path().to_path_buf();
    let log_path = log_file.path().to_path_buf();
    let vc_path = vc_file.path().to_path_buf();

    {
        let data_links = NamedTypesDecorator::new(&data_path, false)?;
        let log_links = NamedTypesDecorator::new(&log_path, false)?;
        let vc_links = NamedTypesDecorator::new(&vc_path, false)?;
        let tx = TransactionsDecorator::new(
            data_links,
            log_links,
            LogRetentionPolicy::default(),
            CommitMode::default(),
            false,
        )?;
        let mut vc = VersionControlDecorator::new(tx, vc_links, false)?;
        vc.create_and_update(0, 0)?;
        vc.tag("checkpoint", None)?;
        vc.branch("feature", None)?;
        vc.save()?;
    }

    let data_links = NamedTypesDecorator::new(&data_path, false)?;
    let log_links = NamedTypesDecorator::new(&log_path, false)?;
    let vc_links = NamedTypesDecorator::new(&vc_path, false)?;
    let tx = TransactionsDecorator::new(
        data_links,
        log_links,
        LogRetentionPolicy::default(),
        CommitMode::default(),
        false,
    )?;
    let reopened = VersionControlDecorator::new(tx, vc_links, false)?;
    assert!(reopened.list_branches().iter().any(|b| b.name == "feature"));
    assert!(reopened.try_get_tag("checkpoint").is_some());
    Ok(())
}

#[test]
fn checkout_out_of_range_throws() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    vc.create_and_update(0, 0)?;
    assert!(vc.checkout(999).is_err());
    Ok(())
}

#[test]
fn duplicate_branch_throws() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    vc.branch("feature", None)?;
    assert!(vc.branch("feature", None).is_err());
    Ok(())
}

#[test]
fn full_stack_acid_rollback_is_atomic_and_isolated() -> Result<()> {
    let (mut vc, _guard) = make_vc();
    let baseline = snapshot(&vc);
    let initial_sequence = vc.current_sequence();

    vc.begin_transaction()?;
    let a = vc.create_and_update(0, 0)?;
    let b = vc.create_and_update(0, 0)?;
    vc.update(a, b, b)?;

    assert!(vc.exists(a));
    assert!(vc.exists(b));
    assert!(vc.begin_transaction().is_err());
    assert!(vc.branch("blocked", None).is_err());

    vc.rollback()?;

    assert_eq!(initial_sequence, vc.current_sequence());
    let main = vc
        .list_branches()
        .into_iter()
        .find(|branch| branch.name == DEFAULT_BRANCH_NAME)
        .expect("main branch must exist");
    assert_eq!(initial_sequence, main.head);
    assert_eq!(baseline, snapshot(&vc));
    Ok(())
}

#[test]
fn full_stack_acid_commit_is_consistent_and_durable_across_reopen() -> Result<()> {
    let data_file = NamedTempFile::new()?;
    let log_file = NamedTempFile::new()?;
    let vc_file = NamedTempFile::new()?;
    let data_path = data_file.path().to_path_buf();
    let log_path = log_file.path().to_path_buf();
    let vc_path = vc_file.path().to_path_buf();

    let (a, b, committed_sequence) = {
        let data_links = NamedTypesDecorator::new(&data_path, false)?;
        let log_links = NamedTypesDecorator::new(&log_path, false)?;
        let vc_links = NamedTypesDecorator::new(&vc_path, false)?;
        let tx = TransactionsDecorator::new(
            data_links,
            log_links,
            LogRetentionPolicy::default(),
            CommitMode::default(),
            false,
        )?;
        let mut vc = VersionControlDecorator::new(tx, vc_links, false)?;

        vc.begin_transaction()?;
        let a = vc.create_and_update(0, 0)?;
        let b = vc.create_and_update(0, 0)?;
        vc.update(a, b, b)?;
        vc.commit()?;

        let committed_sequence = vc.current_sequence();
        assert!(committed_sequence >= 5);
        assert_eq!(
            vc.transactions().last_logged_sequence(),
            vc.transactions().applied_sequence()
        );
        let main = vc
            .list_branches()
            .into_iter()
            .find(|branch| branch.name == DEFAULT_BRANCH_NAME)
            .expect("main branch must exist");
        assert_eq!(committed_sequence, main.head);

        vc.tag("acid-commit", None)?;
        vc.branch("audit", None)?;
        vc.switch_branch("audit")?;
        vc.delete(b)?;
        assert!(!vc.exists(b));

        vc.switch_branch(DEFAULT_BRANCH_NAME)?;
        assert!(vc.exists(a));
        assert!(vc.exists(b));
        let restored = vc.get(a).copied().expect("link a must exist");
        assert_eq!(b, restored.source);
        assert_eq!(b, restored.target);
        vc.save()?;

        (a, b, committed_sequence)
    };

    let data_links = NamedTypesDecorator::new(&data_path, false)?;
    let log_links = NamedTypesDecorator::new(&log_path, false)?;
    let vc_links = NamedTypesDecorator::new(&vc_path, false)?;
    let tx = TransactionsDecorator::new(
        data_links,
        log_links,
        LogRetentionPolicy::default(),
        CommitMode::default(),
        false,
    )?;
    let reopened = VersionControlDecorator::new(tx, vc_links, false)?;

    assert_eq!(
        Some(committed_sequence),
        reopened.try_get_tag("acid-commit")
    );
    assert!(reopened
        .list_branches()
        .iter()
        .any(|branch| branch.name == "audit"));
    assert_eq!(DEFAULT_BRANCH_NAME, reopened.current_branch());
    assert!(reopened.exists(a));
    assert!(reopened.exists(b));
    let restored = reopened.get(a).copied().expect("link a must exist");
    assert_eq!(b, restored.source);
    assert_eq!(b, restored.target);
    assert_eq!(
        reopened.transactions().last_logged_sequence(),
        reopened.transactions().applied_sequence()
    );
    Ok(())
}

fn snapshot(vc: &VersionControlDecorator) -> Vec<Link> {
    let mut links: Vec<Link> = vc.all().into_iter().copied().collect();
    links.sort_by_key(|link| (link.index, link.source, link.target));
    links
}