lex-store 0.11.21

Content-addressed on-disk store for Lex stages, branches, and traces.
Documentation
//! `Store::apply_operation` — the only way to advance a branch
//! head's op (post-#129).

use lex_store::{Operation, OperationKind, StageTransition, Store, DEFAULT_BRANCH};
use std::collections::BTreeSet;

fn fresh() -> (Store, tempfile::TempDir) {
    let tmp = tempfile::tempdir().unwrap();
    let s = Store::open(tmp.path()).unwrap();
    (s, tmp)
}

#[test]
fn apply_operation_advances_head_on_main() {
    let (s, _tmp) = fresh();
    let op = Operation::new(
        OperationKind::AddFunction {
            sig_id: "fac".into(),
            stage_id: "stg-1".into(),
            effects: BTreeSet::new(),
            budget_cost: None,
        },
        [],
    );
    let t = StageTransition::Create {
        sig_id: "fac".into(),
        stage_id: "stg-1".into(),
    };
    let op_id = s.apply_operation(DEFAULT_BRANCH, op, t).unwrap();
    let head = s.branch_head(DEFAULT_BRANCH).unwrap();
    assert_eq!(head.get("fac"), Some(&"stg-1".to_string()));

    // The branch file now exists with this head_op.
    let b = s.get_branch(DEFAULT_BRANCH).unwrap().unwrap();
    assert_eq!(b.head_op.as_deref(), Some(op_id.as_str()));
}

#[test]
fn apply_operation_chains_against_existing_head() {
    let (s, _tmp) = fresh();
    let op1 = Operation::new(
        OperationKind::AddFunction {
            sig_id: "fac".into(),
            stage_id: "stg-1".into(),
            effects: BTreeSet::new(),
            budget_cost: None,
        },
        [],
    );
    let op_id1 = s.apply_operation(DEFAULT_BRANCH, op1, StageTransition::Create {
        sig_id: "fac".into(), stage_id: "stg-1".into(),
    }).unwrap();
    let op2 = Operation::new(
        OperationKind::ModifyBody {
            sig_id: "fac".into(),
            from_stage_id: "stg-1".into(),
            to_stage_id: "stg-2".into(),
            from_budget: None,
            to_budget: None,
        },
        [op_id1.clone()],
    );
    let op_id2 = s.apply_operation(DEFAULT_BRANCH, op2, StageTransition::Replace {
        sig_id: "fac".into(), from: "stg-1".into(), to: "stg-2".into(),
    }).unwrap();
    assert_ne!(op_id1, op_id2);
    let head = s.branch_head(DEFAULT_BRANCH).unwrap();
    assert_eq!(head.get("fac"), Some(&"stg-2".to_string()));
}

#[test]
fn apply_operation_with_stale_parent_errors() {
    let (s, _tmp) = fresh();
    let op1 = Operation::new(
        OperationKind::AddFunction {
            sig_id: "fac".into(),
            stage_id: "stg-1".into(),
            effects: BTreeSet::new(),
            budget_cost: None,
        },
        [],
    );
    s.apply_operation(DEFAULT_BRANCH, op1, StageTransition::Create {
        sig_id: "fac".into(), stage_id: "stg-1".into(),
    }).unwrap();
    // Op claims a different parent than the current head.
    let bogus = Operation::new(
        OperationKind::ModifyBody {
            sig_id: "fac".into(),
            from_stage_id: "stg-1".into(),
            to_stage_id: "stg-2".into(),
            from_budget: None,
            to_budget: None,
        },
        ["someone-else".into()],
    );
    let err = s.apply_operation(DEFAULT_BRANCH, bogus, StageTransition::Replace {
        sig_id: "fac".into(), from: "stg-1".into(), to: "stg-2".into(),
    });
    assert!(err.is_err(), "expected stale-parent rejection");
    // Head is unchanged.
    let head = s.branch_head(DEFAULT_BRANCH).unwrap();
    assert_eq!(head.get("fac"), Some(&"stg-1".to_string()));
}

#[test]
fn apply_operation_against_unknown_branch_does_not_persist() {
    let (s, _tmp) = fresh();
    let op = Operation::new(
        OperationKind::AddFunction {
            sig_id: "fac".into(),
            stage_id: "stg-1".into(),
            effects: BTreeSet::new(),
            budget_cost: None,
        },
        [],
    );
    let err = s.apply_operation("ghost", op, StageTransition::Create {
        sig_id: "fac".into(), stage_id: "stg-1".into(),
    });
    assert!(err.is_err(), "expected UnknownBranch");
    // The ops/ directory should be empty — the pre-check refused to
    // persist anything when the branch doesn't exist.
    let ops_dir = _tmp.path().join("ops");
    if ops_dir.exists() {
        let n = std::fs::read_dir(&ops_dir).unwrap()
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().is_some_and(|x| x == "json"))
            .count();
        assert_eq!(n, 0, "no op file should be persisted against an unknown branch");
    }
}