haematite-cli 0.21.3

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::PathBuf;

use super::{Output, Request, from_hex, parse};

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

fn args(words: &[&str]) -> impl Iterator<Item = String> {
    words
        .iter()
        .map(|word| (*word).to_owned())
        .collect::<Vec<_>>()
        .into_iter()
}

#[test]
fn a_text_key_and_a_hex_key_read_the_same_bytes() -> TestResult {
    let text = parse(args(&["/store", "--key", "ab"]))?;
    let hex = parse(args(&["/store", "--key-hex", "6162", "--hex"]))?;
    assert_eq!(
        text,
        Request {
            data_dir: PathBuf::from("/store"),
            key: b"ab".to_vec(),
            output: Output::Raw,
        }
    );
    assert_eq!(hex.key, text.key);
    assert_eq!(hex.output, Output::Hex);
    Ok(())
}

#[test]
fn malformed_requests_are_refused_by_name() {
    for (words, expected) in [
        (&[][..], "observe-get needs a data directory"),
        (&["/store"][..], "observe-get needs --key or --key-hex"),
        (&["/store", "--key"][..], "--key needs a value"),
        (
            &["/store", "--key", "a", "--key-hex", "61"][..],
            "observe-get takes one key",
        ),
        (
            &["/store", "--key", "a", "--hex", "--hex"][..],
            "--hex is repeated",
        ),
        (&["/store", "--json"][..], "unknown argument \"--json\""),
    ] {
        assert_eq!(
            parse(args(words)).err().as_deref(),
            Some(expected),
            "{words:?}"
        );
    }
}

#[test]
fn hex_keys_must_be_whole_bytes_of_hex_digits() {
    assert_eq!(from_hex("00ff"), Ok(vec![0, 255]));
    assert!(from_hex("").is_err());
    assert!(from_hex("abc").is_err());
    assert!(from_hex("zz").is_err());
}

#[cfg(unix)]
#[test]
fn a_committed_value_is_read_beside_a_live_writer() -> TestResult {
    let directory = tempfile::tempdir()?;
    let config = haematite::DatabaseConfig {
        data_dir: directory.path().to_path_buf(),
        shard_count: 2,
        distributed: None,
        executor_threads: Some(1),
        node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
        retention: haematite::Retention::KeepAll,
    };
    let writer = haematite::Database::create(
        config,
        haematite::RuntimeOptions {
            long_wait_interval: std::time::Duration::from_secs(1),
            reclaim_start: haematite::ReclaimStart::AtOpen,
        },
    )?;
    writer.put(b"seen".to_vec(), b"committed".to_vec())?;
    writer.commit()?;
    writer.put(b"unseen".to_vec(), b"buffered".to_vec())?;
    // The writer stays open: the observer takes no lock and needs none.
    let observer = haematite::ReadOnlyDatabase::open(directory.path())?;
    assert_eq!(observer.get(b"seen")?, Some(b"committed".to_vec()));
    assert_eq!(observer.get(b"unseen")?, None);
    drop(writer);
    Ok(())
}