haematite-cli 0.21.8

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::error::Error;
use std::path::Path;

use haematite::db::DatabaseConfig;
use haematite::store::NodeCacheBudget;

use super::{MergePin, MergePlan, MergeState, Storage, holder_name};
use crate::serve::protocol::{Change, Fork, Mutation, Selection, Version};

type TestResult<T = ()> = Result<T, Box<dyn Error>>;

fn create(path: &Path) -> TestResult<Storage> {
    Ok(Storage::create(
        DatabaseConfig {
            data_dir: path.to_path_buf(),
            shard_count: 1,
            distributed: None,
            executor_threads: Some(1),
            node_cache_budget: Some(NodeCacheBudget::Unlimited),
            retention: haematite::Retention::KeepAll,
        },
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?)
}

fn live(storage: &Storage, branch: &str) -> TestResult<Version> {
    Ok(storage
        .capture(&Selection {
            branch: branch.to_owned(),
            version: None,
        })?
        .version)
}

fn holders(storage: &Storage, id: &str) -> usize {
    ["origin", "source", "target"]
        .iter()
        .filter(|slot| storage.refs.get(&holder_name(id, slot)).is_some())
        .count()
}

fn refusal(result: Result<MergePlan, super::Failure>, accepted: &str) -> TestResult<String> {
    Ok(result.err().ok_or(accepted)?.code)
}

fn state(storage: &Storage, id: &str) -> TestResult<MergeState> {
    Ok(storage.merge_plan(id)?.ok_or("lost merge row")?.state)
}

#[test]
fn merge_holders_survive_reopen_and_release_after_abandon() -> TestResult {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("database");
    let mut storage = create(&path)?;
    let target = MergePin {
        branch: "main".to_owned(),
        version: live(&storage, "main")?,
    };
    let fork = storage.mutate(&Mutation {
        request_id: "fork".to_owned(),
        branch: target.branch.clone(),
        expected: target.version,
        change: Change::Fork(Fork {
            name: "feature".to_owned(),
            from: target.version,
        }),
    })?;
    let source = MergePin {
        branch: fork.branch,
        version: fork.version,
    };
    let plan = storage.merge_open("review", &source, &target)?;
    assert_eq!(plan.state, MergeState::Open);
    assert_eq!(plan.origin, target);
    assert_eq!(storage.merge_open("review", &source, &target)?, plan);
    assert_eq!(holders(&storage, "review"), 3);
    assert_eq!(live(&storage, "main")?, target.version);
    let mut different = source.clone();
    different.version.sequence += 1;
    assert_eq!(
        refusal(
            storage.merge_open("review", &different, &target),
            "identity change accepted"
        )?,
        "merge_identity_conflict"
    );
    assert_eq!(
        refusal(
            storage.merge_open("moved", &different, &target),
            "moved pin accepted"
        )?,
        "merge_pins_moved"
    );
    let mut interrupted = storage.merge_open("interrupted", &source, &target)?;
    interrupted.state = MergeState::Abandoned;
    storage.write_merge_row(&interrupted)?;
    drop(storage);

    let mut storage = Storage::open(
        &path,
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?;
    assert_eq!(storage.merge_plan("review")?.ok_or("lost plan")?, plan);
    assert_eq!(state(&storage, "interrupted")?, MergeState::Abandoned);
    assert_eq!(holders(&storage, "interrupted"), 3);
    storage.merge_abandon("interrupted")?;
    assert_eq!(holders(&storage, "interrupted"), 0);
    assert_eq!(
        storage.merge_abandon("review")?.state,
        MergeState::Abandoned
    );
    assert_eq!(holders(&storage, "review"), 0);
    assert_eq!(
        storage.merge_abandon("review")?.state,
        MergeState::Abandoned
    );
    assert_eq!(
        refusal(
            storage.merge_open("review", &source, &target),
            "abandoned plan reopened"
        )?,
        "merge_abandoned"
    );
    drop(storage);

    let storage = Storage::open(
        &path,
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?;
    assert_eq!(state(&storage, "review")?, MergeState::Abandoned);
    Ok(())
}

const fn fixture_long_wait_interval() -> std::time::Duration {
    const LONG_WAIT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
    LONG_WAIT_INTERVAL
}