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
//! STOR-003 H1a: a service store made under `KeepAll` is moved to reclaiming
//! once, at open; a store the service did not make is never moved.

#[path = "../../../../haematite/tests/common/runtime_options.rs"]
mod runtime_options;

use runtime_options::runtime_options;

use std::error::Error;
use std::path::{Path, PathBuf};

use haematite::Database;
use haematite::db::vacuum::CANONICAL_REFS_DIR;
use haematite::db::{DEFAULT_RECLAIM_MARK_BYTES, DatabaseConfig};
use haematite::store::NodeCacheBudget;

use super::Storage;

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

fn keep_all(data_dir: PathBuf) -> DatabaseConfig {
    DatabaseConfig {
        data_dir,
        shard_count: 2,
        distributed: None,
        executor_threads: Some(1),
        node_cache_budget: Some(NodeCacheBudget::Unlimited),
        retention: haematite::Retention::KeepAll,
    }
}

/// A service store made as every store was before H1a.
fn kept_all_service_store(data_dir: &Path) -> TestResult {
    let created = Storage::create(
        keep_all(data_dir.to_path_buf()),
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?;
    drop(created);
    Ok(())
}

/// The retention the store's config records, read by opening it as it is.
fn mark_bytes(data_dir: &Path) -> Result<Option<u64>, Box<dyn Error>> {
    Ok(Database::open_path_writer(data_dir, runtime_options())?.reclaim_mark_bytes())
}

fn code(opened: Result<Storage, super::Failure>) -> String {
    opened.err().map(|failure| failure.code).unwrap_or_default()
}

/// Fails while the service opens its stores as they were made: a store made
/// under `KeepAll` would never give anything back. The first open moves it
/// to the default bound; the second finds it moved and rewrites nothing.
#[test]
fn a_kept_all_service_store_is_moved_once() -> TestResult {
    let directory = tempfile::tempdir()?;
    let data_dir = directory.path().join("store");
    kept_all_service_store(&data_dir)?;
    assert_eq!(mark_bytes(&data_dir)?, None);
    drop(Storage::open(
        &data_dir,
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?);
    assert_eq!(mark_bytes(&data_dir)?, Some(DEFAULT_RECLAIM_MARK_BYTES));
    let recorded = std::fs::read(data_dir.join("config.json"))?;
    drop(Storage::open(
        &data_dir,
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?);
    assert_eq!(std::fs::read(data_dir.join("config.json"))?, recorded);
    Ok(())
}

/// Fails while a store without the service's record is moved before it is
/// refused: it is refused as not served, and keeps `KeepAll`.
#[test]
fn a_store_the_service_did_not_make_is_refused_and_not_moved() -> TestResult {
    let directory = tempfile::tempdir()?;
    let data_dir = directory.path().join("store");
    drop(Database::create(
        keep_all(data_dir.clone()),
        runtime_options(),
    )?);
    std::fs::create_dir_all(data_dir.join(CANONICAL_REFS_DIR))?;
    assert_eq!(
        code(Storage::open(
            &data_dir,
            NodeCacheBudget::Unlimited,
            fixture_long_wait_interval()
        )),
        "not_served"
    );
    assert_eq!(mark_bytes(&data_dir)?, None);
    Ok(())
}

/// Fails while a torn service record reads as "not made by the service", or
/// is moved past: the open is refused by name and the store keeps `KeepAll`.
#[test]
fn a_torn_service_record_is_refused_by_name_and_not_moved() -> TestResult {
    let directory = tempfile::tempdir()?;
    let data_dir = directory.path().join("store");
    kept_all_service_store(&data_dir)?;
    for entry in std::fs::read_dir(data_dir.join(CANONICAL_REFS_DIR))? {
        let path = entry?.path();
        let bytes = std::fs::read(&path)?;
        let torn = bytes
            .get(..bytes.len().saturating_sub(1))
            .unwrap_or_default();
        std::fs::write(&path, torn)?;
    }
    let refused = code(Storage::open(
        &data_dir,
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    ));
    assert_eq!(refused, "service_record_unreadable");
    assert_eq!(mark_bytes(&data_dir)?, None);
    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
}